diff --git a/.editorconfig b/.editorconfig index 1b369c23..f4307feb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,564 +1,68 @@ +root = true + [*] charset = utf-8 end_of_line = lf -indent_size = 2 -indent_style = space -insert_final_newline = false -max_line_length = 120 -tab_width = 2 -ij_continuation_indent_size = 8 -ij_formatter_off_tag = @formatter:off -ij_formatter_on_tag = @formatter:on -ij_formatter_tags_enabled = false -ij_smart_tabs = false -ij_visual_guides = none -ij_wrap_on_typing = false - -[*.csv] -indent_style = tab -tab_width = 4 -ij_csv_keep_indents_on_empty_lines = true -ij_csv_wrap_long_lines = false - -[*.java] -ij_visual_guides = 80 -ij_java_align_consecutive_assignments = false -ij_java_align_consecutive_variable_declarations = false -ij_java_align_group_field_declarations = true -ij_java_align_multiline_annotation_parameters = true -ij_java_align_multiline_array_initializer_expression = true -ij_java_align_multiline_assignment = false -ij_java_align_multiline_binary_operation = true -ij_java_align_multiline_chained_methods = true -ij_java_align_multiline_deconstruction_list_components = true -ij_java_align_multiline_extends_list = false -ij_java_align_multiline_for = true -ij_java_align_multiline_method_parentheses = false -ij_java_align_multiline_parameters = true -ij_java_align_multiline_parameters_in_calls = true -ij_java_align_multiline_parenthesized_expression = false -ij_java_align_multiline_records = true -ij_java_align_multiline_resources = true -ij_java_align_multiline_ternary_operation = false -ij_java_align_multiline_text_blocks = true -ij_java_align_multiline_throws_list = true -ij_java_align_subsequent_simple_methods = false -ij_java_align_throws_keyword = false -ij_java_align_types_in_multi_catch = true -ij_java_annotation_parameter_wrap = off -ij_java_array_initializer_new_line_after_left_brace = false -ij_java_array_initializer_right_brace_on_new_line = false -ij_java_array_initializer_wrap = off -ij_java_assert_statement_colon_on_next_line = false -ij_java_assert_statement_wrap = off -ij_java_assignment_wrap = off -ij_java_binary_operation_sign_on_next_line = false -ij_java_binary_operation_wrap = off -ij_java_blank_lines_after_anonymous_class_header = 0 -ij_java_blank_lines_after_class_header = 0 -ij_java_blank_lines_after_imports = 1 -ij_java_blank_lines_after_package = 1 -ij_java_blank_lines_around_class = 1 -ij_java_blank_lines_around_field = 0 -ij_java_blank_lines_around_field_in_interface = 0 -ij_java_blank_lines_around_initializer = 1 -ij_java_blank_lines_around_method = 1 -ij_java_blank_lines_around_method_in_interface = 1 -ij_java_blank_lines_before_class_end = 0 -ij_java_blank_lines_before_imports = 1 -ij_java_blank_lines_before_method_body = 0 -ij_java_blank_lines_before_package = 0 -ij_java_block_brace_style = end_of_line -ij_java_block_comment_add_space = false -ij_java_block_comment_at_first_column = true -ij_java_builder_methods = none -ij_java_call_parameters_new_line_after_left_paren = false -ij_java_call_parameters_right_paren_on_new_line = false -ij_java_call_parameters_wrap = off -ij_java_case_statement_on_separate_line = true -ij_java_catch_on_new_line = true -ij_java_class_annotation_wrap = off -ij_java_class_brace_style = end_of_line -ij_java_class_count_to_use_import_on_demand = 50 -ij_java_class_names_in_javadoc = 3 -ij_java_deconstruction_list_wrap = normal -ij_java_do_not_indent_top_level_class_members = false -ij_java_do_not_wrap_after_single_annotation = false -ij_java_do_not_wrap_after_single_annotation_in_parameter = false -ij_java_do_while_brace_force = never -ij_java_doc_add_blank_line_after_description = true -ij_java_doc_add_blank_line_after_param_comments = false -ij_java_doc_add_blank_line_after_return = false -ij_java_doc_add_p_tag_on_empty_lines = true -ij_java_doc_align_exception_comments = true -ij_java_doc_align_param_comments = true -ij_java_doc_do_not_wrap_if_one_line = false -ij_java_doc_enable_formatting = true -ij_java_doc_enable_leading_asterisks = true -ij_java_doc_indent_on_continuation = false -ij_java_doc_keep_empty_lines = true -ij_java_doc_keep_empty_parameter_tag = true -ij_java_doc_keep_empty_return_tag = true -ij_java_doc_keep_empty_throws_tag = true -ij_java_doc_keep_invalid_tags = true -ij_java_doc_param_description_on_new_line = false -ij_java_doc_preserve_line_breaks = false -ij_java_doc_use_throws_not_exception_tag = true -ij_java_else_on_new_line = true -ij_java_enum_constants_wrap = off -ij_java_extends_keyword_wrap = off -ij_java_extends_list_wrap = off -ij_java_field_annotation_wrap = off -ij_java_finally_on_new_line = true -ij_java_for_brace_force = never -ij_java_for_statement_new_line_after_left_paren = false -ij_java_for_statement_right_paren_on_new_line = false -ij_java_for_statement_wrap = off -ij_java_generate_final_locals = false -ij_java_generate_final_parameters = false -ij_java_if_brace_force = never -ij_java_imports_layout = *,|,javax.**,java.**,|,$* -ij_java_indent_case_from_switch = true -ij_java_insert_inner_class_imports = false -ij_java_insert_override_annotation = true -ij_java_keep_blank_lines_before_right_brace = 2 -ij_java_keep_blank_lines_between_package_declaration_and_header = 2 -ij_java_keep_blank_lines_in_code = 2 -ij_java_keep_blank_lines_in_declarations = 2 -ij_java_keep_builder_methods_indents = false -ij_java_keep_control_statement_in_one_line = true -ij_java_keep_first_column_comment = false -ij_java_keep_indents_on_empty_lines = false -ij_java_keep_line_breaks = true -ij_java_keep_multiple_expressions_in_one_line = false -ij_java_keep_simple_blocks_in_one_line = false -ij_java_keep_simple_classes_in_one_line = false -ij_java_keep_simple_lambdas_in_one_line = false -ij_java_keep_simple_methods_in_one_line = false -ij_java_label_indent_absolute = false -ij_java_label_indent_size = 0 -ij_java_lambda_brace_style = end_of_line -ij_java_layout_static_imports_separately = true -ij_java_line_comment_add_space = false -ij_java_line_comment_add_space_on_reformat = false -ij_java_line_comment_at_first_column = true -ij_java_method_annotation_wrap = off -ij_java_method_brace_style = end_of_line -ij_java_method_call_chain_wrap = off -ij_java_method_parameters_new_line_after_left_paren = false -ij_java_method_parameters_right_paren_on_new_line = false -ij_java_method_parameters_wrap = off -ij_java_modifier_list_wrap = false -ij_java_multi_catch_types_wrap = normal -ij_java_names_count_to_use_import_on_demand = 50 -ij_java_new_line_after_lparen_in_annotation = false -ij_java_new_line_after_lparen_in_deconstruction_pattern = true -ij_java_new_line_after_lparen_in_record_header = false -ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.* -ij_java_parameter_annotation_wrap = off -ij_java_parentheses_expression_new_line_after_left_paren = false -ij_java_parentheses_expression_right_paren_on_new_line = false -ij_java_place_assignment_sign_on_next_line = false -ij_java_prefer_longer_names = true -ij_java_prefer_parameters_wrap = false -ij_java_record_components_wrap = off -ij_java_repeat_synchronized = true -ij_java_replace_instanceof_and_cast = false -ij_java_replace_null_check = true -ij_java_replace_sum_lambda_with_method_ref = true -ij_java_resource_list_new_line_after_left_paren = false -ij_java_resource_list_right_paren_on_new_line = false -ij_java_resource_list_wrap = off -ij_java_rparen_on_new_line_in_annotation = false -ij_java_rparen_on_new_line_in_deconstruction_pattern = true -ij_java_rparen_on_new_line_in_record_header = false -ij_java_space_after_closing_angle_bracket_in_type_argument = false -ij_java_space_after_colon = true -ij_java_space_after_comma = true -ij_java_space_after_comma_in_type_arguments = true -ij_java_space_after_for_semicolon = true -ij_java_space_after_quest = true -ij_java_space_after_type_cast = true -ij_java_space_before_annotation_array_initializer_left_brace = false -ij_java_space_before_annotation_parameter_list = false -ij_java_space_before_array_initializer_left_brace = false -ij_java_space_before_catch_keyword = true -ij_java_space_before_catch_left_brace = true -ij_java_space_before_catch_parentheses = true -ij_java_space_before_class_left_brace = true -ij_java_space_before_colon = true -ij_java_space_before_colon_in_foreach = true -ij_java_space_before_comma = false -ij_java_space_before_deconstruction_list = false -ij_java_space_before_do_left_brace = true -ij_java_space_before_else_keyword = true -ij_java_space_before_else_left_brace = true -ij_java_space_before_finally_keyword = true -ij_java_space_before_finally_left_brace = true -ij_java_space_before_for_left_brace = true -ij_java_space_before_for_parentheses = true -ij_java_space_before_for_semicolon = false -ij_java_space_before_if_left_brace = true -ij_java_space_before_if_parentheses = true -ij_java_space_before_method_call_parentheses = false -ij_java_space_before_method_left_brace = true -ij_java_space_before_method_parentheses = false -ij_java_space_before_opening_angle_bracket_in_type_parameter = false -ij_java_space_before_quest = true -ij_java_space_before_switch_left_brace = true -ij_java_space_before_switch_parentheses = true -ij_java_space_before_synchronized_left_brace = true -ij_java_space_before_synchronized_parentheses = true -ij_java_space_before_try_left_brace = true -ij_java_space_before_try_parentheses = true -ij_java_space_before_type_parameter_list = false -ij_java_space_before_while_keyword = true -ij_java_space_before_while_left_brace = true -ij_java_space_before_while_parentheses = true -ij_java_space_inside_one_line_enum_braces = false -ij_java_space_within_empty_array_initializer_braces = false -ij_java_space_within_empty_method_call_parentheses = false -ij_java_space_within_empty_method_parentheses = false -ij_java_spaces_around_additive_operators = true -ij_java_spaces_around_annotation_eq = true -ij_java_spaces_around_assignment_operators = true -ij_java_spaces_around_bitwise_operators = true -ij_java_spaces_around_equality_operators = true -ij_java_spaces_around_lambda_arrow = true -ij_java_spaces_around_logical_operators = true -ij_java_spaces_around_method_ref_dbl_colon = false -ij_java_spaces_around_multiplicative_operators = true -ij_java_spaces_around_relational_operators = true -ij_java_spaces_around_shift_operators = true -ij_java_spaces_around_type_bounds_in_type_parameters = true -ij_java_spaces_around_unary_operator = false -ij_java_spaces_within_angle_brackets = false -ij_java_spaces_within_annotation_parentheses = false -ij_java_spaces_within_array_initializer_braces = false -ij_java_spaces_within_braces = false -ij_java_spaces_within_brackets = false -ij_java_spaces_within_cast_parentheses = false -ij_java_spaces_within_catch_parentheses = false -ij_java_spaces_within_deconstruction_list = false -ij_java_spaces_within_for_parentheses = false -ij_java_spaces_within_if_parentheses = false -ij_java_spaces_within_method_call_parentheses = false -ij_java_spaces_within_method_parentheses = false -ij_java_spaces_within_parentheses = false -ij_java_spaces_within_record_header = false -ij_java_spaces_within_switch_parentheses = false -ij_java_spaces_within_synchronized_parentheses = false -ij_java_spaces_within_try_parentheses = false -ij_java_spaces_within_while_parentheses = false -ij_java_special_else_if_treatment = true -ij_java_subclass_name_suffix = Impl -ij_java_ternary_operation_signs_on_next_line = false -ij_java_ternary_operation_wrap = off -ij_java_test_name_suffix = Test -ij_java_throws_keyword_wrap = off -ij_java_throws_list_wrap = off -ij_java_use_external_annotations = false -ij_java_use_fq_class_names = false -ij_java_use_relative_indents = false -ij_java_use_single_class_imports = true -ij_java_variable_annotation_wrap = off -ij_java_visibility = public -ij_java_while_brace_force = never -ij_java_while_on_new_line = true -ij_java_wrap_comments = false -ij_java_wrap_first_method_in_call_chain = false -ij_java_wrap_long_lines = false - -[*.properties] -ij_properties_align_group_field_declarations = false -ij_properties_keep_blank_lines = false -ij_properties_key_value_delimiter = equals -ij_properties_spaces_around_key_value_delimiter = false - -[.editorconfig] -ij_editorconfig_align_group_field_declarations = false -ij_editorconfig_space_after_colon = false -ij_editorconfig_space_after_comma = true -ij_editorconfig_space_before_colon = false -ij_editorconfig_space_before_comma = false -ij_editorconfig_spaces_around_assignment_operators = true - -[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] -ij_xml_align_attributes = true -ij_xml_align_text = false -ij_xml_attribute_wrap = normal -ij_xml_block_comment_add_space = false -ij_xml_block_comment_at_first_column = true -ij_xml_keep_blank_lines = 2 -ij_xml_keep_indents_on_empty_lines = false -ij_xml_keep_line_breaks = true -ij_xml_keep_line_breaks_in_text = true -ij_xml_keep_whitespaces = false -ij_xml_keep_whitespaces_around_cdata = preserve -ij_xml_keep_whitespaces_inside_cdata = false -ij_xml_line_comment_at_first_column = true -ij_xml_space_after_tag_name = false -ij_xml_space_around_equals_in_attribute = false -ij_xml_space_inside_empty_tag = false -ij_xml_text_wrap = normal - -[{*.bash,*.sh,*.zsh}] -ij_shell_binary_ops_start_line = false -ij_shell_keep_column_alignment_padding = false -ij_shell_minify_program = false -ij_shell_redirect_followed_by_space = false -ij_shell_switch_cases_indented = false -ij_shell_use_unix_line_separator = true +trim_trailing_whitespace = true -[{*.gant,*.groovy,*.gy}] -indent_size = 4 -tab_width = 4 -ij_groovy_align_group_field_declarations = false -ij_groovy_align_multiline_array_initializer_expression = false -ij_groovy_align_multiline_assignment = false -ij_groovy_align_multiline_binary_operation = false -ij_groovy_align_multiline_chained_methods = false -ij_groovy_align_multiline_extends_list = false -ij_groovy_align_multiline_for = true -ij_groovy_align_multiline_list_or_map = true -ij_groovy_align_multiline_method_parentheses = false -ij_groovy_align_multiline_parameters = true -ij_groovy_align_multiline_parameters_in_calls = false -ij_groovy_align_multiline_resources = true -ij_groovy_align_multiline_ternary_operation = false -ij_groovy_align_multiline_throws_list = false -ij_groovy_align_named_args_in_map = true -ij_groovy_align_throws_keyword = false -ij_groovy_array_initializer_new_line_after_left_brace = false -ij_groovy_array_initializer_right_brace_on_new_line = false -ij_groovy_array_initializer_wrap = off -ij_groovy_assert_statement_wrap = off -ij_groovy_assignment_wrap = off -ij_groovy_binary_operation_wrap = off -ij_groovy_blank_lines_after_class_header = 0 -ij_groovy_blank_lines_after_imports = 1 -ij_groovy_blank_lines_after_package = 1 -ij_groovy_blank_lines_around_class = 1 -ij_groovy_blank_lines_around_field = 0 -ij_groovy_blank_lines_around_field_in_interface = 0 -ij_groovy_blank_lines_around_method = 1 -ij_groovy_blank_lines_around_method_in_interface = 1 -ij_groovy_blank_lines_before_imports = 1 -ij_groovy_blank_lines_before_method_body = 0 -ij_groovy_blank_lines_before_package = 0 -ij_groovy_block_brace_style = end_of_line -ij_groovy_block_comment_add_space = false -ij_groovy_block_comment_at_first_column = true -ij_groovy_call_parameters_new_line_after_left_paren = false -ij_groovy_call_parameters_right_paren_on_new_line = false -ij_groovy_call_parameters_wrap = off -ij_groovy_catch_on_new_line = false -ij_groovy_class_annotation_wrap = split_into_lines -ij_groovy_class_brace_style = end_of_line -ij_groovy_class_count_to_use_import_on_demand = 5 -ij_groovy_do_while_brace_force = never -ij_groovy_else_on_new_line = false -ij_groovy_enable_groovydoc_formatting = true -ij_groovy_enum_constants_wrap = off -ij_groovy_extends_keyword_wrap = off -ij_groovy_extends_list_wrap = off -ij_groovy_field_annotation_wrap = split_into_lines -ij_groovy_finally_on_new_line = false -ij_groovy_for_brace_force = never -ij_groovy_for_statement_new_line_after_left_paren = false -ij_groovy_for_statement_right_paren_on_new_line = false -ij_groovy_for_statement_wrap = off -ij_groovy_ginq_general_clause_wrap_policy = 2 -ij_groovy_ginq_having_wrap_policy = 1 -ij_groovy_ginq_indent_having_clause = true -ij_groovy_ginq_indent_on_clause = true -ij_groovy_ginq_on_wrap_policy = 1 -ij_groovy_ginq_space_after_keyword = true -ij_groovy_if_brace_force = never -ij_groovy_import_annotation_wrap = 2 -ij_groovy_imports_layout = *,|,javax.**,java.**,|,$* -ij_groovy_indent_case_from_switch = true -ij_groovy_indent_label_blocks = true -ij_groovy_insert_inner_class_imports = false -ij_groovy_keep_blank_lines_before_right_brace = 2 -ij_groovy_keep_blank_lines_in_code = 2 -ij_groovy_keep_blank_lines_in_declarations = 2 -ij_groovy_keep_control_statement_in_one_line = true -ij_groovy_keep_first_column_comment = true -ij_groovy_keep_indents_on_empty_lines = false -ij_groovy_keep_line_breaks = true -ij_groovy_keep_multiple_expressions_in_one_line = false -ij_groovy_keep_simple_blocks_in_one_line = false -ij_groovy_keep_simple_classes_in_one_line = true -ij_groovy_keep_simple_lambdas_in_one_line = true -ij_groovy_keep_simple_methods_in_one_line = true -ij_groovy_label_indent_absolute = false -ij_groovy_label_indent_size = 0 -ij_groovy_lambda_brace_style = end_of_line -ij_groovy_layout_static_imports_separately = true -ij_groovy_line_comment_add_space = false -ij_groovy_line_comment_add_space_on_reformat = false -ij_groovy_line_comment_at_first_column = true -ij_groovy_method_annotation_wrap = split_into_lines -ij_groovy_method_brace_style = end_of_line -ij_groovy_method_call_chain_wrap = off -ij_groovy_method_parameters_new_line_after_left_paren = false -ij_groovy_method_parameters_right_paren_on_new_line = false -ij_groovy_method_parameters_wrap = off -ij_groovy_modifier_list_wrap = false -ij_groovy_names_count_to_use_import_on_demand = 3 -ij_groovy_packages_to_use_import_on_demand = java.awt.*,javax.swing.* -ij_groovy_parameter_annotation_wrap = off -ij_groovy_parentheses_expression_new_line_after_left_paren = false -ij_groovy_parentheses_expression_right_paren_on_new_line = false -ij_groovy_prefer_parameters_wrap = false -ij_groovy_resource_list_new_line_after_left_paren = false -ij_groovy_resource_list_right_paren_on_new_line = false -ij_groovy_resource_list_wrap = off -ij_groovy_space_after_assert_separator = true -ij_groovy_space_after_colon = true -ij_groovy_space_after_comma = true -ij_groovy_space_after_comma_in_type_arguments = true -ij_groovy_space_after_for_semicolon = true -ij_groovy_space_after_quest = true -ij_groovy_space_after_type_cast = true -ij_groovy_space_before_annotation_parameter_list = false -ij_groovy_space_before_array_initializer_left_brace = false -ij_groovy_space_before_assert_separator = false -ij_groovy_space_before_catch_keyword = true -ij_groovy_space_before_catch_left_brace = true -ij_groovy_space_before_catch_parentheses = true -ij_groovy_space_before_class_left_brace = true -ij_groovy_space_before_closure_left_brace = true -ij_groovy_space_before_colon = true -ij_groovy_space_before_comma = false -ij_groovy_space_before_do_left_brace = true -ij_groovy_space_before_else_keyword = true -ij_groovy_space_before_else_left_brace = true -ij_groovy_space_before_finally_keyword = true -ij_groovy_space_before_finally_left_brace = true -ij_groovy_space_before_for_left_brace = true -ij_groovy_space_before_for_parentheses = true -ij_groovy_space_before_for_semicolon = false -ij_groovy_space_before_if_left_brace = true -ij_groovy_space_before_if_parentheses = true -ij_groovy_space_before_method_call_parentheses = false -ij_groovy_space_before_method_left_brace = true -ij_groovy_space_before_method_parentheses = false -ij_groovy_space_before_quest = true -ij_groovy_space_before_record_parentheses = false -ij_groovy_space_before_switch_left_brace = true -ij_groovy_space_before_switch_parentheses = true -ij_groovy_space_before_synchronized_left_brace = true -ij_groovy_space_before_synchronized_parentheses = true -ij_groovy_space_before_try_left_brace = true -ij_groovy_space_before_try_parentheses = true -ij_groovy_space_before_while_keyword = true -ij_groovy_space_before_while_left_brace = true -ij_groovy_space_before_while_parentheses = true -ij_groovy_space_in_named_argument = true -ij_groovy_space_in_named_argument_before_colon = false -ij_groovy_space_within_empty_array_initializer_braces = false -ij_groovy_space_within_empty_method_call_parentheses = false -ij_groovy_spaces_around_additive_operators = true -ij_groovy_spaces_around_assignment_operators = true -ij_groovy_spaces_around_bitwise_operators = true -ij_groovy_spaces_around_equality_operators = true -ij_groovy_spaces_around_lambda_arrow = true -ij_groovy_spaces_around_logical_operators = true -ij_groovy_spaces_around_multiplicative_operators = true -ij_groovy_spaces_around_regex_operators = true -ij_groovy_spaces_around_relational_operators = true -ij_groovy_spaces_around_shift_operators = true -ij_groovy_spaces_within_annotation_parentheses = false -ij_groovy_spaces_within_array_initializer_braces = false -ij_groovy_spaces_within_braces = true -ij_groovy_spaces_within_brackets = false -ij_groovy_spaces_within_cast_parentheses = false -ij_groovy_spaces_within_catch_parentheses = false -ij_groovy_spaces_within_for_parentheses = false -ij_groovy_spaces_within_gstring_injection_braces = false -ij_groovy_spaces_within_if_parentheses = false -ij_groovy_spaces_within_list_or_map = false -ij_groovy_spaces_within_method_call_parentheses = false -ij_groovy_spaces_within_method_parentheses = false -ij_groovy_spaces_within_parentheses = false -ij_groovy_spaces_within_switch_parentheses = false -ij_groovy_spaces_within_synchronized_parentheses = false -ij_groovy_spaces_within_try_parentheses = false -ij_groovy_spaces_within_tuple_expression = false -ij_groovy_spaces_within_while_parentheses = false -ij_groovy_special_else_if_treatment = true -ij_groovy_ternary_operation_wrap = off -ij_groovy_throws_keyword_wrap = off -ij_groovy_throws_list_wrap = off -ij_groovy_use_flying_geese_braces = false -ij_groovy_use_fq_class_names = false -ij_groovy_use_fq_class_names_in_javadoc = true -ij_groovy_use_relative_indents = false -ij_groovy_use_single_class_imports = true -ij_groovy_variable_annotation_wrap = off -ij_groovy_while_brace_force = never -ij_groovy_while_on_new_line = false -ij_groovy_wrap_chain_calls_after_dot = false -ij_groovy_wrap_long_lines = false - -[{*.gradle.kts,*.kt,*.kts,*.main.kts,*.space.kts}] -max_line_length = 160 +# https://github.com/facebook/ktfmt/blob/main/docs/editorconfig/.editorconfig-google +[{*.kt,*.kts}] +indent_style = space +insert_final_newline = true +max_line_length = 100 +indent_size = 2 +ij_continuation_indent_size = 2 +ij_java_names_count_to_use_import_on_demand = 9999 ij_kotlin_align_in_columns_case_branch = false ij_kotlin_align_multiline_binary_operation = false -ij_kotlin_align_multiline_extends_list = true +ij_kotlin_align_multiline_extends_list = false ij_kotlin_align_multiline_method_parentheses = false ij_kotlin_align_multiline_parameters = true -ij_kotlin_align_multiline_parameters_in_calls = true -ij_kotlin_allow_trailing_comma = false -ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true ij_kotlin_assignment_wrap = normal ij_kotlin_blank_lines_after_class_header = 0 ij_kotlin_blank_lines_around_block_when_branches = 0 ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 -ij_kotlin_block_comment_add_space = false ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_new_line_after_left_paren = true ij_kotlin_call_parameters_right_paren_on_new_line = false -ij_kotlin_call_parameters_wrap = off -ij_kotlin_catch_on_new_line = true -ij_kotlin_class_annotation_wrap = off +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL -ij_kotlin_continuation_indent_for_chained_calls = false -ij_kotlin_continuation_indent_for_expression_bodies = false -ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_for_chained_calls = true +ij_kotlin_continuation_indent_for_expression_bodies = true +ij_kotlin_continuation_indent_in_argument_lists = true ij_kotlin_continuation_indent_in_elvis = false ij_kotlin_continuation_indent_in_if_conditions = false ij_kotlin_continuation_indent_in_parameter_lists = false ij_kotlin_continuation_indent_in_supertype_lists = false -ij_kotlin_else_on_new_line = true +ij_kotlin_else_on_new_line = false ij_kotlin_enum_constants_wrap = off ij_kotlin_extends_list_wrap = normal -ij_kotlin_field_annotation_wrap = off -ij_kotlin_finally_on_new_line = true -ij_kotlin_if_rparen_on_new_line = true +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = false +ij_kotlin_if_rparen_on_new_line = false ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ ij_kotlin_insert_whitespaces_in_simple_one_line_method = true ij_kotlin_keep_blank_lines_before_right_brace = 2 ij_kotlin_keep_blank_lines_in_code = 2 ij_kotlin_keep_blank_lines_in_declarations = 2 -ij_kotlin_keep_first_column_comment = false +ij_kotlin_keep_first_column_comment = true ij_kotlin_keep_indents_on_empty_lines = false ij_kotlin_keep_line_breaks = true ij_kotlin_lbrace_on_next_line = false -ij_kotlin_line_break_after_multiline_when_entry = true ij_kotlin_line_comment_add_space = false -ij_kotlin_line_comment_add_space_on_reformat = false ij_kotlin_line_comment_at_first_column = true -ij_kotlin_method_annotation_wrap = off +ij_kotlin_method_annotation_wrap = split_into_lines ij_kotlin_method_call_chain_wrap = normal -ij_kotlin_method_parameters_new_line_after_left_paren = false -ij_kotlin_method_parameters_right_paren_on_new_line = false -ij_kotlin_method_parameters_wrap = off -ij_kotlin_name_count_to_use_star_import = 2147483647 -ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_name_count_to_use_star_import = 9999 +ij_kotlin_name_count_to_use_star_import_for_members = 9999 ij_kotlin_parameter_annotation_wrap = off ij_kotlin_space_after_comma = true ij_kotlin_space_after_extend_colon = true @@ -583,89 +87,7 @@ ij_kotlin_spaces_around_relational_operators = true ij_kotlin_spaces_around_unary_operator = false ij_kotlin_spaces_around_when_arrow = true ij_kotlin_variable_annotation_wrap = off -ij_kotlin_while_on_new_line = true -ij_kotlin_wrap_elvis_expressions = 0 +ij_kotlin_while_on_new_line = false +ij_kotlin_wrap_elvis_expressions = 1 ij_kotlin_wrap_expression_body_functions = 1 -ij_kotlin_wrap_first_method_in_call_chain = false - -[{*.har,*.json}] -tab_width = 4 -ij_json_array_wrapping = split_into_lines -ij_json_keep_blank_lines_in_code = 0 -ij_json_keep_indents_on_empty_lines = false -ij_json_keep_line_breaks = true -ij_json_keep_trailing_comma = false -ij_json_object_wrapping = split_into_lines -ij_json_property_alignment = do_not_align -ij_json_space_after_colon = true -ij_json_space_after_comma = true -ij_json_space_before_colon = false -ij_json_space_before_comma = false -ij_json_spaces_within_braces = false -ij_json_spaces_within_brackets = false -ij_json_wrap_long_lines = false - -[{*.htm,*.html,*.sht,*.shtm,*.shtml}] -ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 -ij_html_align_attributes = true -ij_html_align_text = false -ij_html_attribute_wrap = normal -ij_html_block_comment_add_space = false -ij_html_block_comment_at_first_column = true -ij_html_do_not_align_children_of_min_lines = 0 -ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p -ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot -ij_html_enforce_quotes = false -ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var -ij_html_keep_blank_lines = 2 -ij_html_keep_indents_on_empty_lines = false -ij_html_keep_line_breaks = true -ij_html_keep_line_breaks_in_text = true -ij_html_keep_whitespaces = false -ij_html_keep_whitespaces_inside = span,pre,textarea -ij_html_line_comment_at_first_column = true -ij_html_new_line_after_last_attribute = never -ij_html_new_line_before_first_attribute = never -ij_html_quote_style = double -ij_html_remove_new_line_before_tags = br -ij_html_space_after_tag_name = false -ij_html_space_around_equality_in_attribute = false -ij_html_space_inside_empty_tag = false -ij_html_text_wrap = normal - -[{*.markdown,*.md}] -indent_size = 4 -tab_width = 4 -ij_markdown_force_one_space_after_blockquote_symbol = true -ij_markdown_force_one_space_after_header_symbol = true -ij_markdown_force_one_space_after_list_bullet = true -ij_markdown_force_one_space_between_words = true -ij_markdown_format_tables = true -ij_markdown_insert_quote_arrows_on_wrap = true -ij_markdown_keep_indents_on_empty_lines = false -ij_markdown_keep_line_breaks_inside_text_blocks = true -ij_markdown_max_lines_around_block_elements = 1 -ij_markdown_max_lines_around_header = 1 -ij_markdown_max_lines_between_paragraphs = 1 -ij_markdown_min_lines_around_block_elements = 1 -ij_markdown_min_lines_around_header = 1 -ij_markdown_min_lines_between_paragraphs = 1 -ij_markdown_wrap_text_if_long = true -ij_markdown_wrap_text_inside_blockquotes = true - -[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] -indent_size = 4 -tab_width = 4 -ij_toml_keep_indents_on_empty_lines = false - -[{*.yaml,*.yml}] -ij_yaml_align_values_properties = do_not_align -ij_yaml_autoinsert_sequence_marker = true -ij_yaml_block_mapping_on_new_line = false -ij_yaml_indent_sequence_value = true -ij_yaml_keep_indents_on_empty_lines = false -ij_yaml_keep_line_breaks = true -ij_yaml_sequence_on_new_line = false -ij_yaml_space_before_colon = false -ij_yaml_spaces_within_braces = true -ij_yaml_spaces_within_brackets = true +ij_kotlin_wrap_first_method_in_call_chain = false \ No newline at end of file diff --git a/.gitignore b/.gitignore index dea9256d..66f06f86 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,10 @@ gradle-app.setting .gradletasknamecache # Mac -.DS_Store \ No newline at end of file +.DS_Store + +# Kotlin +.kotlin/ + +# Java +.java-version \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4639387f..fcbe37d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,28 @@ ### Fixed -## 6.4.0 - 2025-03-18 +## 7.0.0 - 2025-04-27 ### Added -- Compatibility with 2025.1 EAP releases +- All encoding/decoding, escaping/unescaping and text transformation tools now also support reading from and writing to files. +- New "Escape Sequence" tool for escaping/unescaping line breaks, tabs, backslashes, and single/double quotes. +- New "JSON Handling" settings that allow very fine-grained control over the features for reading and writing JSON in all tools. This makes it possible to handle certain non-standard JSON features, such as comments. +- Added a setting to control the number of decimal places in the "Color Picker" tool. + +### Changed + +- Setting "Dialog is modal" was reset to its default value (false), due to overhaul of the internal settings handling. + +### Removed + +- The "Line Breaks Encoding" tool has been replaced by the new "Escape Sequence" tool. + +### Fixed + +- Fixed compatibility problems with 2025.1 releases. +- The `hsl/hsla` CSS color value wasn't correctly calculated in the "Color Picker" tool. +- JSON input errors contained too much irrelevant metadata about the internal JSON processing. ## 6.3.0 - 2025-01-14 @@ -143,7 +160,7 @@ ### Added -- Add automatic input text case detection to the text case converter +- Add automatic input text case detection to the text case converter - Add escape/unescape as editor actions and code intentions - Add new tool: Text Statistic - Add support for the "Dot Text Case" diff --git a/build.gradle.kts b/build.gradle.kts index 3822da04..0e5288f8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,3 @@ - import org.jetbrains.changelog.Changelog import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask @@ -17,17 +16,18 @@ plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.intellij.platform) alias(libs.plugins.changelog) + alias(libs.plugins.spotless) + alias(libs.plugins.version.catalog.update) } -subprojects { - apply(plugin = "org.jetbrains.intellij.platform.module") -} +subprojects { apply(plugin = "org.jetbrains.intellij.platform.module") } val platform = properties("platform") allprojects { apply(plugin = "java") apply(plugin = "kotlin") + apply(plugin = "com.diffplug.spotless") group = properties("pluginGroup") version = properties("pluginVersion") @@ -36,9 +36,7 @@ allprojects { mavenLocal() mavenCentral() - intellijPlatform { - defaultRepositories() - } + intellijPlatform { defaultRepositories() } } dependencies { @@ -51,16 +49,14 @@ allprojects { } } - java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) - } - } + spotless { kotlin { ktfmt().googleStyle() } } + + java { toolchain { languageVersion.set(JavaLanguageVersion.of(21)) } } tasks { withType { compilerOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") + freeCompilerArgs = listOf("-Xjsr305=strict", "-opt-in=kotlin.ExperimentalStdlibApi") jvmTarget.set(JvmTarget.JVM_21) } } @@ -69,6 +65,8 @@ allprojects { useJUnitPlatform() systemProperty("java.awt.headless", "false") } + + named("check") { dependsOn("spotlessCheck") } } } @@ -77,80 +75,69 @@ dependencies { pluginVerifier() zipSigner() - pluginModule(project(":common")) + pluginModule(implementation(project(":common"))) + pluginModule(implementation(project(":settings"))) + pluginModule(implementation(project(":tools-editor"))) + pluginModule(implementation(project(":tools-ui"))) if (platform == "IC") { - pluginModule(project(":java-dependent")) - pluginModule(project(":kotlin-dependent")) + pluginModule(implementation(project(":java-dependent"))) + pluginModule(implementation(project(":kotlin-dependent"))) } } - implementation(libs.bundles.jackson) - implementation(libs.uuid.generator) { - exclude(group = "org.slf4j", module = "slf4j-api") - } - implementation(libs.jsonpath) { - exclude(group = "org.slf4j", module = "slf4j-api") - } - implementation(libs.json.schema.validator) { - exclude("org.apache.commons", "commons-lang3") - exclude(group = "org.slf4j", module = "slf4j-api") - } - implementation(libs.commons.text) - implementation(libs.commons.codec) - implementation(libs.commons.io) - implementation(libs.commons.compress) - implementation(libs.ulid.creator) - implementation(libs.csscolor4j) - implementation(libs.okhttp) - implementation(libs.jfiglet) - implementation(libs.jnanoid) - implementation(libs.jose4j) { - exclude(group = "org.slf4j", module = "slf4j-api") - } - implementation(libs.named.regexp) - implementation(libs.sql.formatter) - implementation(libs.bundles.zxing) - implementation(libs.bundles.text.case.converter) - testImplementation(libs.assertj.core) testImplementation(libs.bundles.junit.implementation) testRuntimeOnly(libs.bundles.junit.runtime) - // Required for the PSI Kotlin structure in tests. It needs to be compile-only - // as some parts are clashing with the IntelliJ platform dependency and - // causing the tests initialisation to fail. - testCompileOnly(libs.kotlin.compiler) + testImplementation(libs.commons.csv) testImplementation(testFixtures(project(":common"))) + testImplementation(testFixtures(project(":tools-ui"))) } intellijPlatform { pluginConfiguration { + id = providers.gradleProperty("pluginId") version = providers.gradleProperty("pluginVersion") + ideaVersion { sinceBuild = properties("pluginSinceBuild") untilBuild = provider { null } } - changeNotes.set(provider { changelog.renderItem(changelog.get(project.version as String), Changelog.OutputType.HTML) }) + + changeNotes.set( + provider { + changelog.renderItem(changelog.get(project.version as String), Changelog.OutputType.HTML) + } + ) } signing { val jetbrainsDir = File(System.getProperty("user.home"), ".jetbrains") - certificateChain.set(project.provider { File(jetbrainsDir, "plugin-sign-chain.crt").readText() }) - privateKey.set(project.provider { File(jetbrainsDir, "plugin-sign-private-key.pem").readText() }) + certificateChain.set( + project.provider { File(jetbrainsDir, "plugin-sign-chain.crt").readText() } + ) + privateKey.set( + project.provider { File(jetbrainsDir, "plugin-sign-private-key.pem").readText() } + ) password.set(project.provider { properties("jetbrains.sign-plugin.password") }) } publishing { token.set(project.provider { properties("jetbrains.marketplace.token") }) - channels.set(listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first())) + channels.set( + listOf(properties("pluginVersion").split('-').getOrElse(1) { "default" }.split('.').first()) + ) } pluginVerification { failureLevel.set( listOf( - COMPATIBILITY_PROBLEMS, INTERNAL_API_USAGES, NON_EXTENDABLE_API_USAGES, - OVERRIDE_ONLY_API_USAGES, INVALID_PLUGIN, + COMPATIBILITY_PROBLEMS, + INTERNAL_API_USAGES, + NON_EXTENDABLE_API_USAGES, + OVERRIDE_ONLY_API_USAGES, + INVALID_PLUGIN, // Will fail for non-IC IDEs - //MISSING_DEPENDENCIES + // MISSING_DEPENDENCIES ) ) @@ -158,7 +145,7 @@ intellijPlatform { recommended() properties("pluginVerificationAdditionalIdes").split(",").forEach { ide -> -// ide(ide, properties("platformVersion")) + ide(ide, properties("platformVersion")) } } } @@ -171,38 +158,14 @@ changelog { groups.set(listOf("Added", "Changed", "Removed", "Fixed")) } -val writeChangelogToFileTask = tasks.create("writeChangelogToFile") { - val generatedResourcesDir = layout.buildDirectory.dir("generated-resources/changelog").get() - outputs.dir(generatedResourcesDir) - - doLast { - val renderResult = changelog.instance.get().releasedItems.joinToString("\n") { changelog.renderItem(it, Changelog.OutputType.HTML) } - val baseDir = generatedResourcesDir.dir("dev/turingcomplete/intellijdevelopertoolsplugin") - file(baseDir).mkdirs() - file(baseDir.file("changelog.html")).writeText(renderResult) - } -} - -sourceSets { - main { - resources { - srcDir(writeChangelogToFileTask) - } - } -} - tasks { named("publishPlugin") { dependsOn("check") - doFirst { - check(platform == "IC") { "Expected platform 'IC', but was: '$platform'" } - } + doFirst { check(platform == "IC") { "Expected platform 'IC', but was: '$platform'" } } } - named("buildSearchableOptions") { - enabled = false - } + named("buildSearchableOptions") { enabled = false } named("runIde") { jvmArgumentProviders += CommandLineArgumentProvider { @@ -210,4 +173,16 @@ tasks { listOf("-Didea.kotlin.plugin.use.k2=true") } } -} \ No newline at end of file +} + +versionCatalogUpdate { + pin { + versions.set( + listOf( + // Must be updated in conjunction with the minimum platform version + // https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library + "kotlin" + ) + ) + } +} diff --git a/gradle.properties b/gradle.properties index 9f142247..e5ece3ad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,13 +1,16 @@ +# The typo in the ID is frustrating, but we will never be able to change it :/ +pluginId=dev.turingcomplete.intellijdevelopertoolsplugins pluginGroup=dev.turingcomplete -pluginVersion=6.4.0 -pluginSinceBuild=251 +pluginVersion=7.0.0 +pluginName=Developer Tools +pluginSinceBuild=243 platform=IC # LATEST-EAP-SNAPSHOT -platformVersion=251.23774-EAP-CANDIDATE-SNAPSHOT +platformVersion=2024.3 platformGlobalBundledPlugins=com.intellij.modules.json pluginVerificationAdditionalIdes=CL # Opt-out flag for bundling Kotlin standard library. -# See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. +# See https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#adding-kotlin-support for details. kotlin.stdlib.default.dependency=false kotlin.daemon.jvmargs=-Xmx2G \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4e2232bb..8b6a421b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,76 +1,98 @@ [versions] -jackson = "2.17.0" -junit = "5.10.2" -zxing = "3.5.3" -text-case-converter = "2.0.0" -uuid-generator = "5.0.0" -jsonpath = "2.9.0" -json-schema-validator = "1.4.0" -commons-text = "1.10.0" -commons-codec = "1.15" -commons-io = "2.15.1" -commons-compress = "1.26.0" -ulid-creator = "5.2.3" +assertj = "3.27.3" +changelog = "2.2.1" +commons-codec = "1.18.0" +commons-compress = "1.27.1" +commons-csv = "1.14.0" +commons-io = "2.19.0" +commons-text = "1.13.1" csscolor4j = "1.0.0" -okhttp = "4.12.0" +intellij-platform = "2.5.0" +jackson = "2.19.0" jfiglet = "0.0.9" jnanoid = "2.0.0" jose4j = "0.9.6" -named-regexp = "1.0.0" -sql-formatter = "2.0.4" -assertj = "3.25.3" +json-schema-validator = "1.5.6" +jsonpath = "2.9.0" +junit4 = "4.13.2" +junit5 = "5.12.2" # See bundled version: https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library -kotlin = "2.1.10" -intellij-platform = "2.3.0" -changelog = "2.2.0" +kotlin = "2.0.21" +named-regexp = "1.0.0" +okhttp = "4.12.0" +spotless = "7.0.3" +version-catalog-update = "1.0.0" +sql-formatter = "2.0.5" +text-case-converter = "2.0.0" +ulid-creator = "5.2.3" +uuid-generator = "5.1.0" +zxing = "3.5.3" [libraries] -jackson-core = { group = "com.fasterxml.jackson.core", name = "jackson-core", version.ref = "jackson" } -jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jackson" } -jackson-dataformat-yaml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-yaml", version.ref = "jackson" } -jackson-dataformat-xml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-xml", version.ref = "jackson" } -jackson-dataformat-properties = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-properties", version.ref = "jackson" } -jackson-dataformat-toml = { group = "com.fasterxml.jackson.dataformat", name = "jackson-dataformat-toml", version.ref = "jackson" } - -uuid-generator = { group = "com.fasterxml.uuid", name = "java-uuid-generator", version.ref = "uuid-generator" } -jsonpath = { group = "com.jayway.jsonpath", name = "json-path", version.ref = "jsonpath" } -json-schema-validator = { group = "com.networknt", name = "json-schema-validator", version.ref = "json-schema-validator" } -commons-text = { group = "org.apache.commons", name = "commons-text", version.ref = "commons-text" } -commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" } -commons-io = { group = "commons-io", name = "commons-io", version.ref = "commons-io" } -commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commons-compress" } -ulid-creator = { group = "com.github.f4b6a3", name = "ulid-creator", version.ref = "ulid-creator" } -csscolor4j = { group = "org.silentsoft", name = "csscolor4j", version.ref = "csscolor4j" } -okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } -jfiglet = { group = "com.github.lalyos", name = "jfiglet", version.ref = "jfiglet" } -jnanoid = { group = "com.aventrix.jnanoid", name = "jnanoid", version.ref = "jnanoid" } -jose4j = { group = "org.bitbucket.b_c", name = "jose4j", version.ref = "jose4j" } -named-regexp = { group = "com.github.tony19", name = "named-regexp", version.ref = "named-regexp" } -sql-formatter = { group = "com.github.vertical-blank", name = "sql-formatter", version.ref = "sql-formatter" } - -zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing" } -zxing-javase = { group = "com.google.zxing", name = "javase", version.ref = "zxing" } - -text-case-converter = { group = "dev.turingcomplete", name = "text-case-converter", version.ref = "text-case-converter" } -text-case-converter-kotlin-extension = { group = "dev.turingcomplete", name = "text-case-converter-kotlin-extension", version.ref = "text-case-converter" } - -assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "assertj" } - -junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junit" } -junit-jupiter-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "junit" } -junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junit" } -junit4 = { group = "junit", name = "junit", version = "4.13.2" } - -kotlin-compiler = { group = "org.jetbrains.kotlin", name = "kotlin-compiler", version.ref = "kotlin" } +assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } +commons-codec = { module = "commons-codec:commons-codec", version.ref = "commons-codec" } +commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commons-compress" } +commons-csv = { module = "org.apache.commons:commons-csv", version.ref = "commons-csv" } +commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" } +commons-text = { module = "org.apache.commons:commons-text", version.ref = "commons-text" } +csscolor4j = { module = "org.silentsoft:csscolor4j", version.ref = "csscolor4j" } +jackson-core = { module = "com.fasterxml.jackson.core:jackson-core", version.ref = "jackson" } +jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } +jackson-dataformat-properties = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-properties", version.ref = "jackson" } +jackson-dataformat-toml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-toml", version.ref = "jackson" } +jackson-dataformat-xml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-xml", version.ref = "jackson" } +jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" } +jfiglet = { module = "com.github.lalyos:jfiglet", version.ref = "jfiglet" } +jnanoid = { module = "com.aventrix.jnanoid:jnanoid", version.ref = "jnanoid" } +jose4j = { module = "org.bitbucket.b_c:jose4j", version.ref = "jose4j" } +json-schema-validator = { module = "com.networknt:json-schema-validator", version.ref = "json-schema-validator" } +jsonpath = { module = "com.jayway.jsonpath:json-path", version.ref = "jsonpath" } +junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" } +# See: https://docs.gradle.org/8.12/userguide/upgrading_version_8.html#manually_declaring_dependencies +junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +junit4 = { module = "junit:junit", version.ref = "junit4" } +named-regexp = { module = "com.github.tony19:named-regexp", version.ref = "named-regexp" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +sql-formatter = { module = "com.github.vertical-blank:sql-formatter", version.ref = "sql-formatter" } +text-case-converter = { module = "dev.turingcomplete:text-case-converter", version.ref = "text-case-converter" } +text-case-converter-kotlin-extension = { module = "dev.turingcomplete:text-case-converter-kotlin-extension", version.ref = "text-case-converter" } +ulid-creator = { module = "com.github.f4b6a3:ulid-creator", version.ref = "ulid-creator" } +uuid-generator = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "uuid-generator" } +zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" } +zxing-javase = { module = "com.google.zxing:javase", version.ref = "zxing" } [bundles] -zxing = ["zxing-core", "zxing-javase"] -jackson = ["jackson-core", "jackson-databind", "jackson-dataformat-yaml", "jackson-dataformat-xml", "jackson-dataformat-properties", "jackson-dataformat-toml"] -junit-implementation = ["junit-jupiter-api", "junit-jupiter-params"] -junit-runtime = ["junit-jupiter-engine", "junit4"] -text-case-converter = ["text-case-converter", "text-case-converter-kotlin-extension"] +jackson = [ + "jackson-core", + "jackson-databind", + "jackson-dataformat-properties", + "jackson-dataformat-toml", + "jackson-dataformat-xml", + "jackson-dataformat-yaml", +] +junit-implementation = [ + "junit-jupiter-api", + "junit-jupiter-params", +] +junit-runtime = [ + "junit-jupiter-engine", + "junit-platform-launcher", + "junit4", +] +text-case-converter = [ + "text-case-converter", + "text-case-converter-kotlin-extension", +] +zxing = [ + "zxing-core", + "zxing-javase", +] [plugins] -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } intellij-platform = { id = "org.jetbrains.intellij.platform", version.ref = "intellij-platform" } -changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } \ No newline at end of file +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } +version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } diff --git a/modules/common/build.gradle.kts b/modules/common/build.gradle.kts index 677744ff..0f5ca7ef 100644 --- a/modules/common/build.gradle.kts +++ b/modules/common/build.gradle.kts @@ -1,15 +1,44 @@ +import org.jetbrains.kotlin.gradle.utils.extendsFrom + plugins { `java-test-fixtures` } dependencies { - implementation(libs.commons.text) - implementation(libs.bundles.text.case.converter) + api(libs.bundles.text.case.converter) + api(libs.okhttp) testImplementation(libs.assertj.core) testImplementation(libs.bundles.junit.implementation) testRuntimeOnly(libs.bundles.junit.runtime) + intellijPlatform { testBundledPlugins("org.jetbrains.kotlin") } + configurations.testFixturesApi.extendsFrom(configurations.intellijPlatformTestBundledPlugins) + testFixturesImplementation(libs.assertj.core) testFixturesImplementation(libs.bundles.junit.implementation) -} \ No newline at end of file +} + +val generatePluginProperties by + tasks.registering { + inputs.property("pluginId", project.property("pluginId")) + inputs.property("pluginVersion", project.property("pluginVersion")) + + val outputDir = layout.buildDirectory.dir("generated-resources") + outputs.dir(outputDir) + + doLast { + val file = outputDir.get().file("plugin.properties").asFile + file.parentFile.mkdirs() + file.writeText( + """ + pluginId=${project.property("pluginId")} + pluginVersion=${project.property("pluginVersion")} + pluginName=${project.property("pluginName")} + """ + .trimIndent() + ) + } + } + +tasks.named("processResources") { from(generatePluginProperties.map { it.outputs.files }) } diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/CopyValuesAction.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CopyValuesAction.kt similarity index 60% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/CopyValuesAction.kt rename to modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CopyValuesAction.kt index 9b390e84..197a2089 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/CopyValuesAction.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CopyValuesAction.kt @@ -1,11 +1,11 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.common import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.project.DumbAwareAction import com.intellij.util.PlatformIcons -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.PluginCommonDataKeys.SELECTED_VALUES +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginCommonDataKeys.SELECTED_VALUES import java.awt.datatransfer.StringSelection import javax.swing.Icon @@ -13,15 +13,16 @@ class CopyValuesAction( private val singleValue: String = "Copy Value", private val pluralValue: (Int) -> String = { "Copy $it Values" }, private val valueToString: (Any) -> String? = { it.toString() }, - icon: Icon? = PlatformIcons.COPY_ICON + icon: Icon? = PlatformIcons.COPY_ICON, ) : DumbAwareAction(singleValue, null, icon) { - // -- Companion Object -------------------------------------------------------------------------------------------- // - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun update(e: AnActionEvent) { - val selectedValues = SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") + val selectedValues = + SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") val nonBlankValues = selectedValues.count { valueToString(it)?.isNotBlank() ?: false } e.presentation.isVisible = nonBlankValues >= 1 @@ -29,16 +30,22 @@ class CopyValuesAction( } override fun actionPerformed(e: AnActionEvent) { - val values: List = SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") + val values: List = + SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") if (values.isEmpty()) { return } - CopyPasteManager.getInstance().setContents(StringSelection(values.mapNotNull { valueToString(it) }.joinToString(System.lineSeparator()))) + CopyPasteManager.getInstance() + .setContents( + StringSelection( + values.mapNotNull { valueToString(it) }.joinToString(System.lineSeparator()) + ) + ) } override fun getActionUpdateThread() = ActionUpdateThread.EDT - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtils.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtils.kt index 264d4c9a..eb22b3c3 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtils.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtils.kt @@ -1,20 +1,28 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.common +import java.security.Provider +import java.security.Security import java.security.spec.PKCS8EncodedKeySpec -import java.util.* +import java.util.Base64 object CryptoUtils { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val RAW_KEY_REGEX = Regex("\\r?\\n|\\r|\\s?-+(BEGIN|END).*KEY-+\\s?") - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun toRkcs8Key(keyInput: String): PKCS8EncodedKeySpec = PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyInput.replace(RAW_KEY_REGEX, ""))) + fun registerBouncyCastleProvider() { + val bouncyCastleProviderClass = + Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider") + val bouncyCastleProvider = bouncyCastleProviderClass.getConstructor().newInstance() + Security.addProvider(bouncyCastleProvider as Provider) + } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/EditorUtils.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/EditorUtils.kt index 5911ddf0..8bfba91c 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/EditorUtils.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/EditorUtils.kt @@ -9,9 +9,9 @@ import com.intellij.openapi.editor.SelectionModel import com.intellij.openapi.util.TextRange object EditorUtils { - // -- Variables --------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Variables ----------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun Editor.getSelectedText(): Pair? { val selectionModel = this.selectionModel @@ -21,25 +21,27 @@ object EditorUtils { } fun Editor.executeWriteCommand(actionName: String, action: (Editor) -> Unit) { - CommandProcessor.getInstance().executeCommand(this.project, { - runWriteAction { - action(this) - } - }, actionName, null, this.document) + CommandProcessor.getInstance() + .executeCommand( + this.project, + { runWriteAction { action(this) } }, + actionName, + null, + this.document, + ) } fun AnActionEvent.getEditor(): Editor = this.getData(CommonDataKeys.EDITOR) ?: error("Editor not found") - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun SelectionModel.getTextRangeIfSelection(): TextRange? = if (selectionStart < selectionEnd) { TextRange(selectionStart, selectionEnd) - } - else { + } else { null } - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/GitHubUtils.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/GitHubUtils.kt new file mode 100644 index 00000000..ad47c032 --- /dev/null +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/GitHubUtils.kt @@ -0,0 +1,138 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.common + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.common.OkHttpClientUtils.applyIntelliJProxySettings +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import okhttp3.OkHttpClient +import okhttp3.Request + +object GitHubUtils { + // -- Properties ---------------------------------------------------------- // + + private val log = logger() + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun downloadFiles( + project: Project, + repositoryUrl: String, + destinationPath: Path, + preDownloadFilter: (String) -> Boolean, + afterDownloadFilter: (Path) -> Boolean, + onStart: () -> Unit, + onSuccess: () -> Unit, + onThrowable: (Throwable) -> Unit, + onFinished: () -> Unit, + ) = + object : Task.Backgroundable(project, "Downloading files from GitHub") { + + override fun run(indicator: ProgressIndicator) { + onStart() + indicator.isIndeterminate = true + + val httpClient = OkHttpClient.Builder().applyIntelliJProxySettings(repositoryUrl).build() + + val fileNamesToDownloadUrls = + fetchFiles(httpClient, repositoryUrl).filter { preDownloadFilter(it.key) } + indicator.isIndeterminate = false + + var index = 1 + var errors = 0 + fileNamesToDownloadUrls.forEach { fileName, downloadUrl -> + if (indicator.isCanceled) { + return@forEach + } + + indicator.text = "Downloading $fileName" + indicator.fraction = index.toDouble() / fileNamesToDownloadUrls.size + + val targetPath = destinationPath.resolve(fileName) + val success = downloadFile(httpClient, downloadUrl, targetPath) + if (!success) { + errors++ + } else { + indicator.text = "Analyzing $fileName" + val keepFile = afterDownloadFilter(targetPath) + if (!keepFile) { + try { + Files.deleteIfExists(targetPath) + } catch (e: Exception) { + log.warn("Failed to delete file: $targetPath", e) + } + } + } + index++ + } + if (errors > 0) { + throw Exception("Failed to download $errors of ${fileNamesToDownloadUrls.size} files") + } + indicator.text = "All files downloaded" + } + + override fun onThrowable(error: Throwable) { + onThrowable(error) + } + + override fun onFinished() { + onFinished() + } + + override fun onSuccess() { + onSuccess() + } + } + + private fun fetchFiles(httpClient: OkHttpClient, repositoryUrl: String): Map { + val apiUrl = + repositoryUrl.replace("https://github.com/", "https://api.github.com/repos/") + "/contents" + val request = + Request.Builder().get().url(apiUrl).header("Accept", "application/vnd.github.v3+json").build() + + val response = httpClient.newCall(request).execute() + if (response.code == 200) { + response.body?.byteStream()?.use { inputStream -> + val rootNode: JsonNode = ObjectMapper().readTree(inputStream) + return rootNode + .filter { it["type"].asText() == "file" } + .associate { it["name"].asText() to it["download_url"].asText() } + } + throw Exception("No HTTP body received from $apiUrl") + } else { + throw Exception("Failed to fetch file list from $apiUrl: HTTP ${response.code}") + } + } + + private fun downloadFile( + httpClient: OkHttpClient, + downloadUrl: String, + targetPath: Path, + ): Boolean { + val request = Request.Builder().get().url(downloadUrl).build() + + log.info("Downloading $downloadUrl to $targetPath") + + val response = httpClient.newCall(request).execute() + return if (response.code == 200) { + response.body?.byteStream()?.use { inputStream -> + Files.createDirectories(targetPath.parent) + Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING) + true + } == true + } else { + log.warn("Failed to download $downloadUrl to $targetPath: HTTP ${response.code}") + false + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/HashingUtils.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/HashingUtils.kt index 3da17507..ac0f9d70 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/HashingUtils.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/HashingUtils.kt @@ -4,22 +4,22 @@ import java.security.MessageDigest import java.security.Security object HashingUtils { - // -- Variables --------------------------------------------------------------------------------------------------- // + // -- Variables ----------------------------------------------------------- // val commonMessageDigests: List - private val algorithms = listOf("MD5, SHA-1", "SHA-256", "SHA-512", "SHA3-256", "SHA3-512") + private val algorithms = + listOf("MD5", "SHA-1", "SHA-256", "SHA-512", "SHA3-256", "SHA3-512") - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { val availableAlgorithms: Set = Security.getAlgorithms("MessageDigest") - commonMessageDigests = algorithms.mapNotNull { - if (availableAlgorithms.contains(it)) it.toMessageDigest() else null - } + commonMessageDigests = + algorithms.mapNotNull { if (availableAlgorithms.contains(it)) it.toMessageDigest() else null } } - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/IoExtensions.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/IoExtensions.kt index d573433c..df0b8880 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/IoExtensions.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/IoExtensions.kt @@ -1,11 +1,13 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.common +import java.io.InputStream import java.nio.file.Files import java.nio.file.Path +import java.util.Properties -// -- Properties ---------------------------------------------------------------------------------------------------- // -// -- Initialization ------------------------------------------------------------------------------------------------ // -// -- Exported Methods ---------------------------------------------------------------------------------------------- // +// -- Properties ---------------------------------------------------------- // +// -- Initialization ------------------------------------------------------ // +// -- Exported Methods ---------------------------------------------------- // fun Path.clearDirectory() { if (!Files.exists(this) || !Files.isDirectory(this)) { @@ -23,9 +25,11 @@ fun Path.clearDirectory() { } } -fun Path.nameWithoutExtension() = fileName.toString().substringBeforeLast('.') +fun Path.nameWithoutExtension(): String = fileName.toString().substringBeforeLast('.') -fun Path.extension() = fileName.toString().substringAfterLast('.') +fun Path.extension(): String = fileName.toString().substringAfterLast('.') -// -- Private Methods ----------------------------------------------------------------------------------------------- // -// -- Inner Type ---------------------------------------------------------------------------------------------------- // \ No newline at end of file +fun InputStream.readProperties(): Properties = this.use { Properties().apply { load(it) } } + +// -- Private Methods ---------------------------------------------------- // +// -- Inner Type ---------------------------------------------------------- // diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/KotlinExtensions.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/KotlinExtensions.kt index 1e024d1c..32e759e0 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/KotlinExtensions.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/KotlinExtensions.kt @@ -1,16 +1,26 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.common -import io.ktor.util.* +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.util.transform +import java.math.BigDecimal import java.security.MessageDigest -import java.util.* +import java.util.Base64 +import java.util.HexFormat import kotlin.reflect.KClass import kotlin.reflect.cast -// -- Properties ---------------------------------------------------------------------------------------------------- // +// -- Properties ---------------------------------------------------------- // + +val longMaxValue = BigDecimal(Long.MAX_VALUE) +val longMinValue = BigDecimal(Long.MIN_VALUE) + +val emptyByteArray = ByteArray(0) private val asciiEncodedRegex = "\\\\u([0-9a-fA-F]{4})".toRegex() -// -- Exposed Methods ----------------------------------------------------------------------------------------------- // +// -- Initialization ------------------------------------------------------ // + +// -- Exported Methods ---------------------------------------------------- // inline fun Any.safeCastTo(): T? = this as? T @@ -18,19 +28,31 @@ fun Any.uncheckedCastTo(type: KClass): T = type.cast(this) inline fun Any.uncheckedCastTo(): T = this as T -fun ByteArray.toHexMacAddress() = StringBuilder(18).also { - for (byte in this) { - if (isNotEmpty()) { - it.append(':') +fun ByteArray.toHexMacAddress() = + StringBuilder(18) + .also { + for (byte in this) { + if (isNotEmpty()) { + it.append(':') + } + it.append(String.format("%02x", byte)) + } } - it.append(String.format("%02x", byte)) - } -}.toString() + .toString() fun ByteArray.toHexString(): String = HexFormat.of().formatHex(this) fun String.toMessageDigest(): MessageDigest = MessageDigest.getInstance(this) +fun String.toLowerCasePreservingASCIIRules(): String = + this.map { + when (it) { + in 'A'..'Z' -> it + 32 + else -> it + } + } + .joinToString("") + fun Comparator.makeCaseInsensitive(): Comparator { return Comparator { a, b -> this.compare(a.toLowerCasePreservingASCIIRules(), b.toLowerCasePreservingASCIIRules()) @@ -40,22 +62,24 @@ fun Comparator.makeCaseInsensitive(): Comparator { fun Long?.compareTo(other: Long?): Int { return if (this == null && other == null) { 0 - } - else if (this == null) { + } else if (this == null) { 1 - } - else if (other == null) { + } else if (other == null) { -1 - } - else { + } else { this.compareTo(other) } } fun String.encodeToAscii() = - this.map { - if (it.code > 127) "\\u%04x".format(it.code) else it.toString() - }.joinToString("") + this.map { if (it.code > 127) "\\u%04x".format(it.code) else it.toString() }.joinToString("") + +fun ByteArray.encodeToAscii(): ByteArray = + this.joinToString("") { + if (it.toInt() and 0xFF > 127) "\\u%04x".format(it.toInt() and 0xFF) + else it.toInt().toChar().toString() + } + .toByteArray() fun String.decodeFromAscii() = asciiEncodedRegex.replace(this) { matchResult -> @@ -63,8 +87,43 @@ fun String.decodeFromAscii() = charCode.toChar().toString() } +fun ByteArray.decodeFromAscii(): ByteArray = + asciiEncodedRegex + .replace(this.toString(Charsets.UTF_8)) { matchResult -> + val charCode = matchResult.groupValues[1].toInt(16) + charCode.toChar().toString() + } + .toByteArray() + +fun String.decodeBase64String(): String = + if (this.isNotEmpty()) String(Base64.getDecoder().decode(this)) else "" + fun MatchGroupCollection.getOrNull(index: Int): MatchGroup? = if (index in 0 until size) this[index] else null -// -- Private Methods ----------------------------------------------------------------------------------------------- // -// -- Type ---------------------------------------------------------------------------------------------------------- // +fun > KClass.findEnumValueByName(name: String): T? = + this.java.enumConstants?.find { it.name == name } + +fun > KClass.getEnumValueByNameOrThrow(name: String): T = + this.java.enumConstants?.find { it.name == name } ?: error("Enum value $name not found in $this") + +operator fun ObservableMutableProperty.not(): ObservableMutableProperty = + transform({ !it }) { !it } + +fun BigDecimal.isWithinLongRange(): Boolean = (this <= longMaxValue) && (this >= longMinValue) + +fun > KClass.valueOf(name: String): T { + return this.java.enumConstants?.firstOrNull { it.name == name } + ?: error("Enum $this does not have a constant with name: $name") +} + +inline fun Iterable.ifNotEmpty(action: Iterable.() -> U): U? = + if (this.any()) action(this) else null + +inline fun Collection.random(except: (T) -> Boolean): T = + this.filter { !except(it) }.random() + +inline fun Array.random(except: (T) -> Boolean): T = this.filter { !except(it) }.random() + +// -- Private Methods ---------------------------------------------------- // +// -- Inner Type ---------------------------------------------------------- // diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/LocaleContainer.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/LocaleContainer.kt new file mode 100644 index 00000000..ea8457b2 --- /dev/null +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/LocaleContainer.kt @@ -0,0 +1,30 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.common + +import com.intellij.openapi.ui.naturalSorted +import java.util.Locale + +/** + * A wrapper class for a [java.util.Locale]. This is required because the search function of the + * `comboBox` in the UI DSL does work on the human-readable the display name. It will use the + * `toString()` (e.g., `de_DE`) representation of the [java.util.Locale] instead. + */ +data class LocaleContainer(val locale: Locale) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun toString(): String = locale.displayName + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + val ALL_AVAILABLE_LOCALES = + Locale.getAvailableLocales() + .filter { it.displayName.isNotBlank() } + .map { LocaleContainer(it) } + .naturalSorted() + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/OkHttpClientUtils.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/OkHttpClientUtils.kt similarity index 91% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/OkHttpClientUtils.kt rename to modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/OkHttpClientUtils.kt index f3988e03..5a207365 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/OkHttpClientUtils.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/OkHttpClientUtils.kt @@ -1,19 +1,19 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.common import com.intellij.openapi.vfs.VfsUtil import com.intellij.util.net.JdkProxyProvider -import okhttp3.OkHttpClient -import okhttp3.Protocol import java.io.IOException import java.net.Proxy import java.net.ProxySelector import java.net.SocketAddress import java.net.URI -import java.util.* +import java.util.Collections +import okhttp3.OkHttpClient +import okhttp3.Protocol object OkHttpClientUtils { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // fun OkHttpClient.Builder.applyIntelliJProxySettings(url: String): OkHttpClient.Builder { val proxies = JdkProxyProvider.getInstance().proxySelector.select(VfsUtil.toUri(url)) @@ -98,8 +98,8 @@ object OkHttpClientUtils { else -> null } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class FallbackProxySelector(private val proxies: List) : ProxySelector() { private val failedProxies = Collections.synchronizedSet(mutableSetOf()) @@ -112,4 +112,4 @@ object OkHttpClientUtils { proxies.find { it.address() == sa }?.let { failedProxies.add(it) } } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/PluginCommonDataKeys.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/PluginCommonDataKeys.kt similarity index 58% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/PluginCommonDataKeys.kt rename to modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/PluginCommonDataKeys.kt index a077113b..28de27fb 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/PluginCommonDataKeys.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/PluginCommonDataKeys.kt @@ -1,14 +1,14 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.common import com.intellij.openapi.actionSystem.DataKey object PluginCommonDataKeys { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // val SELECTED_VALUES: DataKey> = DataKey.create("DeveloperToolsPlugin.selectedValues") - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/PluginInfo.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/PluginInfo.kt new file mode 100644 index 00000000..8daf7582 --- /dev/null +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/PluginInfo.kt @@ -0,0 +1,41 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.common + +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginInfo.PluginVersion.Companion.toPluginVersion +import java.util.Properties + +object PluginInfo { + // -- Properties ---------------------------------------------------------- // + + private val pluginProperties: Properties by lazy { + PluginInfo::class.java.classLoader.getResourceAsStream("plugin.properties")!!.readProperties() + } + + val pluginId: String by lazy { pluginProperties.getProperty("pluginId") } + val pluginVersion: PluginVersion by lazy { + pluginProperties.getProperty("pluginVersion").toPluginVersion() + } + val pluginName: String by lazy { pluginProperties.getProperty("pluginName") } + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + data class PluginVersion(val major: Int, val minor: Int, val patch: Int) : + Comparable { + + override fun compareTo(other: PluginVersion): Int = + compareValuesBy(this, other, PluginVersion::major, PluginVersion::minor, PluginVersion::patch) + + override fun toString(): String = "$major.$minor.$patch" + + companion object { + + fun String.toPluginVersion(): PluginVersion { + return this.split('.').let { (major, minor, patch) -> + PluginVersion(major.toInt(), minor.toInt(), patch.toInt()) + } + } + } + } +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/TextCaseUtils.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/TextCaseUtils.kt index c676ae21..8e5e0236 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/TextCaseUtils.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/TextCaseUtils.kt @@ -6,25 +6,25 @@ import dev.turingcomplete.textcaseconverter.TextCase import dev.turingcomplete.textcaseconverter.WordsSplitter object TextCaseUtils { - // -- Variables --------------------------------------------------------------------------------------------------- // + // -- Variables ----------------------------------------------------------- // private val spacesPattern = Regex("\\s+") - private val textCasesWithWithoutSplitWords = setOf( - StandardTextCases.LOWER_CASE, - StandardTextCases.UPPER_CASE, - StandardTextCases.INVERTED_CASE, - StandardTextCases.ALTERNATING_CASE - ) + private val textCasesWithoutSplitWords = + setOf( + StandardTextCases.LOWER_CASE, + StandardTextCases.UPPER_CASE, + StandardTextCases.INVERTED_CASE, + StandardTextCases.ALTERNATING_CASE, + ) - val allTextCases = StandardTextCases.ALL_STANDARD_TEXT_CASES - .sortedWith(sortTextCases()) + val allTextCases = StandardTextCases.ALL_STANDARD_TEXT_CASES.sortedWith(sortTextCases()) - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun determineWordsSplitter(text: String, targetTextCase: TextCase): WordsSplitter { - if (textCasesWithWithoutSplitWords.contains(targetTextCase)) { + if (textCasesWithoutSplitWords.contains(targetTextCase)) { return StandardWordsSplitters.NOOP } @@ -36,24 +36,25 @@ object TextCaseUtils { } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun sortTextCases() = compareBy { - val primaryTextCases = listOf( - StandardTextCases.SCREAMING_SNAKE_CASE, - StandardTextCases.SOFT_CAMEL_CASE, - StandardTextCases.STRICT_CAMEL_CASE, - StandardTextCases.PASCAL_CASE, - StandardTextCases.SNAKE_CASE, - StandardTextCases.KEBAB_CASE - ) - if (it in primaryTextCases) { - primaryTextCases.indexOf(it) + // -- Private Methods ----------------------------------------------------- // + + private fun sortTextCases() = + compareBy { + val primaryTextCases = + listOf( + StandardTextCases.SCREAMING_SNAKE_CASE, + StandardTextCases.SOFT_CAMEL_CASE, + StandardTextCases.STRICT_CAMEL_CASE, + StandardTextCases.PASCAL_CASE, + StandardTextCases.SNAKE_CASE, + StandardTextCases.KEBAB_CASE, + ) + if (it in primaryTextCases) { + primaryTextCases.indexOf(it) + } else { + primaryTextCases.size + StandardTextCases.ALL_STANDARD_TEXT_CASES.indexOf(it) + } } - else { - primaryTextCases.size + StandardTextCases.ALL_STANDARD_TEXT_CASES.indexOf(it) - } - } - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/TextStatisticUtils.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/TextStatisticUtils.kt similarity index 85% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/TextStatisticUtils.kt rename to modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/TextStatisticUtils.kt index 8fe13f8e..5b10c936 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/TextStatisticUtils.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/TextStatisticUtils.kt @@ -1,9 +1,9 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool +package dev.turingcomplete.intellijdevelopertoolsplugin.common object TextStatisticUtils { - // -- Variables --------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Variables ----------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun gatherStatistic(text: String): TextStatistic { var characterCount = 0 @@ -47,7 +47,10 @@ object TextStatisticUtils { } character.isWhitespace() -> { whitespaceCount++ - if (character == '\n' || (character == '\r' && index + 1 < text.length && text[index + 1] == '\n')) + if ( + character == '\n' || + (character == '\r' && index + 1 < text.length && text[index + 1] == '\n') + ) lineBreakCount++ if (wordBuffer.isNotEmpty()) { wordsCount++ @@ -92,7 +95,8 @@ object TextStatisticUtils { wordsPerSentenceSum += sentence.split("\\s+".toRegex()).size } } - val averageWordsPerSentence = if (sentencesCount > 0) wordsPerSentenceSum.toDouble() / sentencesCount else 0.0 + val averageWordsPerSentence = + if (sentencesCount > 0) wordsPerSentenceSum.toDouble() / sentencesCount else 0.0 val paragraphsCount = text.split("\\n\\r?\\n".toRegex()).size @@ -110,12 +114,12 @@ object TextStatisticUtils { averageWordLength = averageWordLength, averageWordsPerSentence = averageWordsPerSentence, paragraphsCount = paragraphsCount, - isoControlCharactersCount = isoControlCharactersCount + isoControlCharactersCount = isoControlCharactersCount, ) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // data class TextStatistic( val charactersCount: Int = 0, @@ -131,6 +135,6 @@ object TextStatisticUtils { val averageWordLength: Double = 0.0, val averageWordsPerSentence: Double = 0.0, val paragraphsCount: Int = 0, - val isoControlCharactersCount: Int = 0 + val isoControlCharactersCount: Int = 0, ) -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ValueProperty.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/ValueProperty.kt similarity index 66% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ValueProperty.kt rename to modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/ValueProperty.kt index 08445501..8ff91e19 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ValueProperty.kt +++ b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/ValueProperty.kt @@ -1,20 +1,20 @@ -@file:Suppress("UnstableApiUsage") - -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.common import com.intellij.openapi.Disposable import com.intellij.openapi.observable.dispatcher.SingleEventDispatcher import com.intellij.openapi.observable.properties.ObservableMutableProperty import java.util.concurrent.atomic.AtomicReference -class ValueProperty(initialValue: T) : ObservableMutableProperty { - // -- Properties -------------------------------------------------------------------------------------------------- // +open class ValueProperty(initialValue: T) : ObservableMutableProperty { + // -- Properties ---------------------------------------------------------- // private val value = AtomicReference(initialValue) private val changeDispatcher = SingleEventDispatcher.create>() + var modificationsCounter: Int = 0 + private set - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun afterChange(parentDisposable: Disposable?, listener: (T) -> Unit) = changeDispatcher.whenEventHappened(parentDisposable) { listener(it.newValue) } @@ -26,15 +26,19 @@ class ValueProperty(initialValue: T) : ObservableMutableProperty { set(value, null) } - internal fun setWithUncheckedCast(value: Any, changeId: String?) { - @Suppress("UNCHECKED_CAST") - set(value as T, changeId) + fun setWithUncheckedCast(value: Any, changeId: String?) { + @Suppress("UNCHECKED_CAST") set(value as T, changeId) } - fun set(value: T, changeId: String?, fireEvent: Boolean = true) { - val oldValue = this.value.getAndSet(value) + fun set(newValue: T, changeId: String?, fireEvent: Boolean = true) { + val oldValue = this.value.getAndSet(newValue) + + if (oldValue != newValue) { + modificationsCounter++ + } + if (fireEvent) { - changeDispatcher.fireEvent(ChangeEvent(changeId, oldValue, value)) + changeDispatcher.fireEvent(ChangeEvent(changeId, oldValue, newValue)) } } @@ -42,18 +46,18 @@ class ValueProperty(initialValue: T) : ObservableMutableProperty { override fun toString(): String = get().toString() - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // data class ChangeEvent(val id: String?, val oldValue: T, val newValue: T) { fun valueChanged() = oldValue != newValue } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { const val RESET_CHANGE_ID = "reset" } -} \ No newline at end of file +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/EscapersUnescapers.kt b/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/EscapersUnescapers.kt deleted file mode 100644 index 9fe4eb3e..00000000 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/EscapersUnescapers.kt +++ /dev/null @@ -1,83 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.common.tool - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.util.TextRange -import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.executeWriteCommand -import org.apache.commons.text.StringEscapeUtils - -internal object EscapersUnescapers { - // -- Variables --------------------------------------------------------------------------------------------------- // - - private val log = logger() - - val commonEscaper = listOf( - Escaper("Java String", { StringEscapeUtils.escapeJava(it) }), - Escaper("HTML Entities", { StringEscapeUtils.escapeHtml4(it) }), - Escaper("JSON Value", { StringEscapeUtils.escapeJson(it) }), - Escaper("XML Value", { StringEscapeUtils.escapeXml11(it) }), - Escaper("CSV Value", { StringEscapeUtils.escapeCsv(it) }) - ) - - val commonUnescaper = listOf( - Unescaper("Java String", { StringEscapeUtils.unescapeJava(it) }), - Unescaper("HTML Entities", { StringEscapeUtils.escapeHtml4(it) }), - Unescaper("JSON Value", { StringEscapeUtils.unescapeJson(it) }), - Unescaper("XML Value", { StringEscapeUtils.unescapeCsv(it) }), - Unescaper("CSV Value", { StringEscapeUtils.unescapeCsv(it) }) - ) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - fun executeEscapeInEditor( - text: String, - textRange: TextRange, - escaper: Escaper, - editor: Editor - ) { - try { - val result = escaper.escape(text) - editor.executeWriteCommand(escaper.actionName) { - it.document.replaceString(textRange.startOffset, textRange.endOffset, result) - } - } - catch (e: Exception) { - log.warn("Escape failed", e) - ApplicationManager.getApplication().invokeLater { - Messages.showErrorDialog(editor.project, "Escape failed: ${e.message}", escaper.actionName) - } - } - } - - fun executeUnescapeInEditor( - text: String, - textRange: TextRange, - unescaper: Unescaper, - editor: Editor - ) { - try { - val result = unescaper.unescape(text) - editor.executeWriteCommand(unescaper.actionName) { - it.document.replaceString(textRange.startOffset, textRange.endOffset, result) - } - } - catch (e: Exception) { - log.warn("Unescape failed", e) - ApplicationManager.getApplication().invokeLater { - Messages.showErrorDialog(editor.project, "Unescape failed: ${e.message}", unescaper.actionName) - } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Escaper(val title: String, val escape: (String) -> String, val actionName: String = "Escape $title") - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Unescaper(val title: String, val unescape: (String) -> String, val actionName: String = "Unescape $title") -} \ No newline at end of file diff --git a/modules/common/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtilsTest.kt b/modules/common/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtilsTest.kt index 4e951a22..b32a4c41 100644 --- a/modules/common/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtilsTest.kt +++ b/modules/common/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/CryptoUtilsTest.kt @@ -1,16 +1,16 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.common +import java.security.spec.PKCS8EncodedKeySpec import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatCode import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource -import java.security.spec.PKCS8EncodedKeySpec class CryptoUtilsTest { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // @ParameterizedTest @MethodSource("validPrivateKeys") @@ -23,21 +23,19 @@ class CryptoUtilsTest { @ParameterizedTest @MethodSource("invalidPrivateKeys") fun `parsing of invalid PEM formatted keys`(invalidPemKey: String) { - assertThatCode { - CryptoUtils.toRkcs8Key(invalidPemKey) - }.doesNotThrowAnyException() + assertThatCode { CryptoUtils.toRkcs8Key(invalidPemKey) }.doesNotThrowAnyException() } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @JvmStatic fun validPrivateKeys(): Collection = listOf( - """ + """ -----BEGIN PRIVATE KEY----- MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W ZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zD @@ -45,30 +43,35 @@ class CryptoUtilsTest { cxMEk5u1bJJNV9IpTey4PPZ0ddOmFGAiAiEA8z3ibztuj9HbW1vJZTZUB3W3uyhH 6uv3g9jPH0FcV00CIQDNzFqJk2ql+0N+2/tHkD3A0P3AUKPd0QPoRBDeyQ== -----END PRIVATE KEY----- - """.trimIndent(), """ + .trimIndent(), + """ -----BEGIN PRIVATE KEY----- MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W - + ZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zD - + UQIDAQABAkAcQ0hOM2j1dLD+Rl4nQUcxdLKHykHpeNkKccJcMqRm7C9P0hxPjTvV -----END PRIVATE KEY----- - """.trimIndent(), - "MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W", - "MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/WZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zDUQIDAQABAkAcQ0hOM2j1dLD+Rl4nQUcxdLKHykHpeNkKccJcMqRm7C9P0hxPjTvV" - ).map { Arguments.of(it) } + """ + .trimIndent(), + "MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/W", + "MIIBVwIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAzPj2X1dyZzDmqT/WZcdQF9i9vL+UN65wX5i2+Zm3B4TFjQtqYSbdTkCGhPrTfKZ1mH6fHnCqJce5Y4zDUQIDAQABAkAcQ0hOM2j1dLD+Rl4nQUcxdLKHykHpeNkKccJcMqRm7C9P0hxPjTvV", + ) + .map { Arguments.of(it) } @JvmStatic fun invalidPrivateKeys(): Collection = listOf( - """ + """ -----BEGIN PRIVATE KEY----- InvalidBase64Content -----END PRIVATE KEY----- - """.trimIndent(), - "InvalidBase64Content", - "" - ).map { Arguments.of(it) } + """ + .trimIndent(), + "InvalidBase64Content", + "", + ) + .map { Arguments.of(it) } } -} \ No newline at end of file +} diff --git a/modules/common/src/testFixtures/kotlin/DescriptionTest.kt b/modules/common/src/testFixtures/kotlin/DescriptionTest.kt deleted file mode 100644 index 87358a4e..00000000 --- a/modules/common/src/testFixtures/kotlin/DescriptionTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.Arguments.arguments -import java.lang.reflect.Modifier -import java.nio.file.Paths -import kotlin.io.path.isRegularFile -import kotlin.io.path.listDirectoryEntries -import kotlin.io.path.nameWithoutExtension - -abstract class DescriptionTest { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - abstract fun testIntentionActionDescriptionHtmlFileExist(intentionActionSimpleClassName: String) - - fun doTestIntentionActionDescriptionHtmlFileExist(intentionActionSimpleClassName: String) { - val classLoader = Thread.currentThread().contextClassLoader - val descriptionHtmlResourcePath = "intentionDescriptions/$intentionActionSimpleClassName/description.html" - val packageResource = classLoader.getResource(descriptionHtmlResourcePath) - assertThat(packageResource).describedAs(descriptionHtmlResourcePath).isNotNull() - } - - abstract fun testIntentionActionBeforeTemplateFileExist(intentionActionSimpleClassName: String) - - fun doTestIntentionActionBeforeTemplateFileExist(intentionActionSimpleClassName: String) { - val classLoader = Thread.currentThread().contextClassLoader - val languageInfix = if (intentionActionSimpleClassName.contains("Kotlin")) "kt" else "java" - val descriptionHtmlResourcePath = "intentionDescriptions/$intentionActionSimpleClassName/before.$languageInfix.template" - val packageResource = classLoader.getResource(descriptionHtmlResourcePath) - assertThat(packageResource).describedAs(descriptionHtmlResourcePath).isNotNull() - } - - abstract fun testIntentionActionAfterTemplateFileExist(intentionActionSimpleClassName: String) - - fun doTestIntentionActionAfterTemplateFileExist(intentionActionSimpleClassName: String) { - val classLoader = Thread.currentThread().contextClassLoader - val languageInfix = if (intentionActionSimpleClassName.contains("Kotlin")) "kt" else "java" - val descriptionHtmlResourcePath = "intentionDescriptions/$intentionActionSimpleClassName/after.$languageInfix.template" - val packageResource = classLoader.getResource(descriptionHtmlResourcePath) - assertThat(packageResource).describedAs(descriptionHtmlResourcePath).isNotNull() - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - fun intentionActionSimpleClassNames(intentionClassesPackage: String): List { - val intentionActionSimpleClassName = Paths.get("src/main/kotlin").resolve(Paths.get(intentionClassesPackage.replace(".", "/"))) - .let { println(it); it } - .listDirectoryEntries() - .filter { it.isRegularFile() && !it.fileName.toString().contains("$") } - .map { Class.forName("$intentionClassesPackage.${it.nameWithoutExtension}") } - .filter { !Modifier.isAbstract(it.modifiers) } - .map { it.simpleName } - assertThat(intentionActionSimpleClassName).hasSizeGreaterThanOrEqualTo(1) - return intentionActionSimpleClassName.map { arguments(it) } - } - } -} \ No newline at end of file diff --git a/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/BundleMessagesTest.kt b/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/BundleMessagesTest.kt new file mode 100644 index 00000000..a1cb6389 --- /dev/null +++ b/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/BundleMessagesTest.kt @@ -0,0 +1,268 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.ui.UIBundle +import dev.turingcomplete.intellijdevelopertoolsplugin.common.extension +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.IoUtils.collectAllFiles +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import java.util.Properties +import kotlin.io.path.nameWithoutExtension +import kotlin.reflect.KClass +import org.assertj.core.api.Assertions.assertThat +import org.jetbrains.kotlin.psi.KtCallExpression +import org.jetbrains.kotlin.psi.KtDotQualifiedExpression +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNameReferenceExpression +import org.jetbrains.kotlin.psi.KtStringTemplateExpression +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DynamicContainer.dynamicContainer +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.DynamicTest.dynamicTest + +abstract class BundleMessagesTest : IdeaTest() { + // -- Properties ---------------------------------------------------------- // + + private val psiManager: PsiManager by lazy { PsiManager.getInstance(fixture.project) } + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + abstract fun `test that all additional languages are containing the same message keys and parameter counts`(): + List + + protected fun `do test that all additional languages are containing the same message keys and parameter counts`(): + List = + messagesBundles.map { + val additionalLanguageToMessages: Map> = + it.languageToMessages.filter { it.key != REFERENCE_LANGUAGE_KEY } + dynamicContainer( + it.bundleName, + additionalLanguageToMessages.map { (additionalLanguageKey, additionalLanguageMessages) -> + val testSameMessageKeys = + dynamicTest("Same message keys") { + assertThat(it.referenceLanguageMessages().keys) + .containsExactlyInAnyOrderElementsOf(additionalLanguageMessages.keys) + } + val sameParameterCounts = + dynamicContainer( + "Language messages have same parameter counts", + additionalLanguageMessages.map { (key, _) -> + dynamicTest(key) { + assertThat(countUniqueParameters(additionalLanguageMessages[key]!!)) + .isEqualTo(countUniqueParameters(it.referenceLanguageMessages()[key]!!)) + } + }, + ) + dynamicContainer(additionalLanguageKey, listOf(testSameMessageKeys, sameParameterCounts)) + }, + ) + } + + abstract fun `test that all message(messageKey, params) calls are referencing to an existing message key and are using the correct parameters count`(): + List + + protected fun `do test that all message(messageKey, params) calls are referencing to an existing message key and are using the correct parameters count`(): + List = + collectAllMessageBundleUsages() + .groupBy { it.className } + .map { (className, allMessageBundleUsagesInClass) -> + dynamicContainer( + className, + allMessageBundleUsagesInClass + .filter { !internalBundleNames.contains(it.bundleName) } + .groupBy { it.bundleName } + .map { (bundleName, allMessageBundleUsages) -> + val messagesBundleReferenceLanguageMessages = + getMessagesBundle(bundleName).referenceLanguageMessages() + + val bundleExistsTest = + dynamicTest("Bundle `$bundleName` exists") { + assertThat(messagesBundles).anyMatch { it.bundleName == bundleName } + } + + val messageKeysAndParametersCountTests = + allMessageBundleUsages.map { messageBundleUsage -> + dynamicTest(messageBundleUsage.displayableText) { + assertThat(messagesBundleReferenceLanguageMessages) + .containsKey(messageBundleUsage.messageKey) + assertThat(messageBundleUsage.parametersCount) + .isEqualTo( + countUniqueParameters( + messagesBundleReferenceLanguageMessages[messageBundleUsage.messageKey]!! + ) + ) + } + } + + dynamicContainer( + bundleName, + listOf(bundleExistsTest) + messageKeysAndParametersCountTests, + ) + }, + ) + } + + abstract fun `test that all keys in the messages bundle are used`(): List + + protected fun `do test that all keys in the messages bundle are used`(): List { + val usedMessageKeys = collectAllMessageBundleUsages().map { it.messageKey } + return messagesBundles.map { messageBundle -> + dynamicContainer( + messageBundle.bundleName, + messageBundle.referenceLanguageMessages().keys.map { + dynamicTest(it) { + assertThat(usedMessageKeys).describedAs("No unused messages bundle keys").contains(it) + } + }, + ) + } + } + + /** + * This base method only collects all calls to `$BundleName.message(messageKey, ...params)`, if + * the `messageKey` is a [String]. Overridden methods may add additional [MessagesBundleUsage]. + */ + protected open fun collectAllMessageBundleUsages(): List = + kotlinSourceFilesToScanForMessagesBundleUsages().flatMap { kotlinSourceFile -> + kotlinSourceFile.toKtFile().traverseElement(KtDotQualifiedExpression::class).mapNotNull { + ktDotQualifiedExpression -> + val receiver = ktDotQualifiedExpression.receiverExpression + if (receiver !is KtNameReferenceExpression) return@mapNotNull null + + val referenceName = receiver.getReferencedName() + val callExpression = ktDotQualifiedExpression.selectorExpression as? KtCallExpression + val methodName = callExpression?.calleeExpression?.text + if (methodName != "message") { + return@mapNotNull null + } + + val parameters = callExpression.valueArguments + val firstArgExpression = + parameters.getOrNull(0)?.getArgumentExpression() as? KtStringTemplateExpression + ?: return@mapNotNull null + + // Reject if the string contains interpolation + if (firstArgExpression.hasInterpolation()) { + return@mapNotNull null + } + + val messageKey = firstArgExpression.entries.joinToString("") { it.text } + + return@mapNotNull MessagesBundleUsage( + className = kotlinSourceFile.nameWithoutExtension, + bundleName = referenceName, + messageKey = messageKey, + parametersCount = parameters.size - 1, + ) + } + } + + protected fun Path.toKtFile(): KtFile { + val virtualFile = LocalFileSystem.getInstance().findFileByNioFile(this)!! + return ApplicationManager.getApplication().runReadAction { + psiManager.findFile(virtualFile) + } as KtFile + } + + protected fun getMessagesBundle(bundleName: String) = + messagesBundles.firstOrNull { it.bundleName == bundleName } + ?: error("Bundle `$bundleName` not found") + + protected open fun kotlinSourceFilesToScanForMessagesBundleUsages(): List { + val classFiles = + Paths.get("src/main/kotlin").collectAllFiles().filter { + setOf("kt", "kts").contains(it.extension()) + } + assertThat(classFiles).hasSizeGreaterThanOrEqualTo(1) + return classFiles + } + + protected fun KtElement.traverseElement(type: KClass): List = + PsiTreeUtil.collectElementsOfType(this, type.java).toList() + + // -- Private Methods ----------------------------------------------------- // + + private fun countUniqueParameters(message: String): Int = + Regex("(?> = + mutableMapOf>(), + ) { + + fun referenceLanguageMessages(): Map = + languageToMessages[REFERENCE_LANGUAGE_KEY] + ?: error("Bundle `$bundleName` is missing the reference language") + } + + // -- Inner Type ---------------------------------------------------------- // + + data class MessagesBundleUsage( + val className: String, + val bundleName: String, + val messageKey: String, + val parametersCount: Int, + val displayableText: String = messageKey, + ) + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + lateinit var messagesBundles: List + private set + + lateinit var kotlinSourceFiles: List + private set + + private const val REFERENCE_LANGUAGE_KEY = "en" + + private val internalBundleNames = setOf(UIBundle::class.simpleName!!) + + @BeforeAll + @JvmStatic + fun setUp() { + messagesBundles = collectAllMessageBundles(File("src/main/resources/message")) + assertThat(messagesBundles).hasSizeGreaterThanOrEqualTo(1) + + kotlinSourceFiles = + Paths.get("src/main/kotlin").collectAllFiles().filter { it.extension() == "kt" } + assertThat(kotlinSourceFiles).hasSizeGreaterThanOrEqualTo(1) + } + + private fun collectAllMessageBundles(messagesBundlesDir: File): List { + check(messagesBundlesDir.isDirectory) + + val result = mutableMapOf() + + messagesBundlesDir + .listFiles { file -> file.extension == "properties" } + .forEach { file -> + val filename = file.nameWithoutExtension + val parts = filename.split("_") + + val bundleName = if (parts.size > 1) parts[0] else filename + val languageKey = if (parts.size > 1) parts[1] else REFERENCE_LANGUAGE_KEY + val properties = Properties().apply { file.inputStream().use { load(it) } } + + val messagesBundle = result.getOrPut(bundleName) { MessagesBundle(bundleName) } + messagesBundle.languageToMessages.put( + languageKey, + properties.stringPropertyNames().associate { it to properties.getProperty(it) }, + ) + } + + return result.values.toList() + } + } +} diff --git a/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IdeaTest.kt b/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IdeaTest.kt new file mode 100644 index 00000000..919b3b14 --- /dev/null +++ b/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IdeaTest.kt @@ -0,0 +1,71 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures + +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.testFramework.fixtures.IdeaProjectTestFixture +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory +import com.intellij.testFramework.junit5.RunInEdt +import com.intellij.testFramework.junit5.RunMethodInEdt +import com.intellij.testFramework.junit5.TestApplication +import dev.turingcomplete.intellijdevelopertoolsplugin.common.CryptoUtils.registerBouncyCastleProvider +import kotlin.reflect.KClass +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.BeforeEach + +@RunInEdt(allMethods = false) +@TestApplication +abstract class IdeaTest { + // -- Properties ---------------------------------------------------------- // + + protected lateinit var fixture: IdeaProjectTestFixture + protected lateinit var disposable: TestDisposable + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @BeforeEach + fun beforeEach() { + disposable = TestDisposable(this::class) + fixture = IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder("test").fixture + fixture.setUp() + } + + @AfterEach + @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) + fun afterEach() { + Disposer.dispose(disposable) + fixture.tearDown() + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + class TestDisposable(val id: String) : Disposable { + + constructor(testClass: KClass<*>) : this(id = testClass.simpleName!!) + + override fun dispose() {} + + override fun toString(): String = id + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + @BeforeAll + @JvmStatic + fun beforeAll() { + registerBouncyCastleProvider() + System.setProperty("java.awt.headless", "true") + } + + @AfterAll + @JvmStatic + fun afterAll() { + System.setProperty("java.awt.headless", "false") + } + } +} diff --git a/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IntentionDescriptionTest.kt b/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IntentionDescriptionTest.kt new file mode 100644 index 00000000..29dc351a --- /dev/null +++ b/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IntentionDescriptionTest.kt @@ -0,0 +1,88 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures + +import com.intellij.codeInsight.intention.IntentionAction +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.IoUtils.collectAllFiles +import java.io.File +import java.lang.reflect.Modifier +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.DynamicTest + +abstract class IntentionDescriptionTest { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + abstract fun `test intention action description HTML file exists`(): List + + fun `do test intention action description HTML file exists`(): List = + getAllIntentionActionClasses() + .map { "/intentionDescriptions/${it.simpleName}/description.html" } + .map { + DynamicTest.dynamicTest(it) { + val descriptionHtml = this::class.java.getResource(it) + assertThat(descriptionHtml) + .describedAs("Intention action description HTML file `${it}` exists") + .isNotNull() + } + } + + abstract fun `test intention action before template file exists`(): List + + fun `do test intention action before template file exists`(): List = + getAllIntentionActionClasses() + .map { getDescriptionTemplateRelativePath(it.simpleName, "before") } + .map { DynamicTest.dynamicTest(it) { testDescriptionTemplateExists(it) } } + + abstract fun `test intention action after template file exists`(): List + + fun `do test intention action after template file exists`(): List = + getAllIntentionActionClasses() + .map { getDescriptionTemplateRelativePath(it.simpleName, "after") } + .map { DynamicTest.dynamicTest(it) { testDescriptionTemplateExists(it) } } + + // -- Private Methods ----------------------------------------------------- // + + private fun getDescriptionTemplateRelativePath( + intentionActionSimpleClassName: String, + templatePosition: String, + ): String { + val languageInfix = if (intentionActionSimpleClassName.contains("Kotlin")) "kt" else "java" + return "/intentionDescriptions/$intentionActionSimpleClassName/$templatePosition.$languageInfix.template" + } + + private fun testDescriptionTemplateExists(descriptionTemplateRelativePath: String) { + val descriptionTemplate = this::class.java.getResource(descriptionTemplateRelativePath) + assertThat(descriptionTemplate) + .describedAs("Description template `$descriptionTemplateRelativePath` exists") + .isNotNull() + } + + private fun getAllIntentionActionClasses(): List> { + val srcMainKotlinDir = File("src/main/kotlin") + val intentionActions = + srcMainKotlinDir + .collectAllFiles() + .filter { it.extension == "kt" } + .map { + // To fully qualified class name + it.absolutePath + .removePrefix("${srcMainKotlinDir.absolutePath}/") + .removeSuffix(".kt") + .replace('/', '.') + } + .map { + // Fix nested class name + it.replace('$', '.') + } + .map { Class.forName(it) } + .filter { !Modifier.isAbstract(it.modifiers) } + .filter { IntentionAction::class.java.isAssignableFrom(it) } + .toList() + assertThat(intentionActions).hasSizeGreaterThanOrEqualTo(1) + return intentionActions + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IoUtils.kt b/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IoUtils.kt new file mode 100644 index 00000000..7ea1fb1a --- /dev/null +++ b/modules/common/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/testfixtures/IoUtils.kt @@ -0,0 +1,40 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures + +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import kotlin.io.path.isRegularFile + +object IoUtils { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun File.collectAllFiles(): List { + check(this.isDirectory) + + val files = mutableListOf() + + this.listFiles()?.forEach { file -> + if (file.isDirectory) { + files.addAll(file.collectAllFiles()) + } else { + files.add(file) + } + } + + return files + } + + fun Path.collectAllFiles(): List { + check(this.exists()) { "Path `$this` does not exists" } + check(this.isDirectory()) { "Path `$this` is not a directory" } + + return Files.walk(this).filter { it.isRegularFile() }.toList() + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/java-dependent/build.gradle.kts b/modules/java-dependent/build.gradle.kts index 4d3e453d..ed743f2a 100644 --- a/modules/java-dependent/build.gradle.kts +++ b/modules/java-dependent/build.gradle.kts @@ -1,11 +1,22 @@ +repositories { + mavenLocal() + mavenCentral() + + intellijPlatform { + defaultRepositories() + } +} + dependencies { intellijPlatform { bundledPlugins("com.intellij.java") } implementation(project(":common")) + implementation(project(":tools-editor")) + testImplementation(libs.assertj.core) testImplementation(libs.bundles.junit.implementation) testRuntimeOnly(libs.bundles.junit.runtime) testImplementation(testFixtures(project(":common"))) -} \ No newline at end of file +} diff --git a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/PsiJavaUtils.kt b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/PsiJavaUtils.kt index 3318390c..377b7d75 100644 --- a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/PsiJavaUtils.kt +++ b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/PsiJavaUtils.kt @@ -10,10 +10,10 @@ import com.intellij.psi.PsiJavaToken import com.intellij.psi.util.elementType import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.getSelectedText -internal object PsiJavaUtils { - // -- Variables --------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +object PsiJavaUtils { + // -- Variables ----------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun getPsiElementAtCaret(e: AnActionEvent): PsiElement? { val psiFile = e.getData(CommonDataKeys.PSI_FILE) ?: return null @@ -29,8 +29,7 @@ internal object PsiJavaUtils { return getTextIfStringValue(psiElement) ?: if (psiElement is PsiIdentifier) { psiElement.text to psiElement.textRange - } - else { + } else { null } } @@ -42,15 +41,13 @@ internal object PsiJavaUtils { val newEnd = psiElement.textRange.endOffset - 1 if (newStart > newEnd) { null - } - else { + } else { psiElement.text.substring(1, psiElement.text.length - 1) to TextRange(newStart, newEnd) } - } - else { + } else { null } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/EncodeDecodeJavaCodeActionGroup.kt b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/EncodeDecodeJavaCodeActionGroup.kt index f1b2d93a..800d4bdd 100644 --- a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/EncodeDecodeJavaCodeActionGroup.kt +++ b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/EncodeDecodeJavaCodeActionGroup.kt @@ -2,18 +2,18 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.tool.edito import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.util.TextRange -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action.EncodeDecodeActionGroup import dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.PsiJavaUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action.EncodeDecodeActionGroup -internal class EncodeDecodeJavaCodeActionGroup : EncodeDecodeActionGroup() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EncodeDecodeJavaCodeActionGroup : EncodeDecodeActionGroup() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getSourceText(e: AnActionEvent): Pair? = PsiJavaUtils.getPsiElementAtCaret(e)?.let { PsiJavaUtils.getTextIfStringValueOrIdentifier(it) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/EscapeUnescapeJavaCodeActionGroup.kt b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/EscapeUnescapeJavaCodeActionGroup.kt index a9a5b4dd..56f2f32f 100644 --- a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/EscapeUnescapeJavaCodeActionGroup.kt +++ b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/EscapeUnescapeJavaCodeActionGroup.kt @@ -2,18 +2,18 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.tool.edito import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.util.TextRange -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action.EscapeUnescapeActionGroup import dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.PsiJavaUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action.EscapeUnescapeActionGroup -internal class EscapeUnescapeJavaCodeActionGroup : EscapeUnescapeActionGroup() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EscapeUnescapeJavaCodeActionGroup : EscapeUnescapeActionGroup() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getSourceText(e: AnActionEvent): Pair? = PsiJavaUtils.getPsiElementAtCaret(e)?.let { PsiJavaUtils.getTextIfStringValue(it) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/TextCaseConverterJavaCodeActionGroup.kt b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/TextCaseConverterJavaCodeActionGroup.kt index 007b1da2..394391d0 100644 --- a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/TextCaseConverterJavaCodeActionGroup.kt +++ b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/action/TextCaseConverterJavaCodeActionGroup.kt @@ -2,18 +2,18 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.tool.edito import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.util.TextRange -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action.TextCaseConverterActionGroup import dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.PsiJavaUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action.TextCaseConverterActionGroup -internal class TextCaseConverterJavaCodeActionGroup : TextCaseConverterActionGroup() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class TextCaseConverterJavaCodeActionGroup : TextCaseConverterActionGroup() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getSourceText(e: AnActionEvent): Pair? = PsiJavaUtils.getPsiElementAtCaret(e)?.let { PsiJavaUtils.getTextIfStringValueOrIdentifier(it) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/EncodeDecodeJavaCodeIntentionAction.kt b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/EncodeDecodeJavaCodeIntentionAction.kt index 60449428..d9e53981 100644 --- a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/EncodeDecodeJavaCodeIntentionAction.kt +++ b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/EncodeDecodeJavaCodeIntentionAction.kt @@ -3,13 +3,13 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.tool.edito import com.intellij.openapi.editor.Editor import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention.EncodeDecodeIntentionAction import dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.PsiJavaUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention.EncodeDecodeIntentionAction -internal class EncodeDecodeJavaCodeIntentionAction : EncodeDecodeIntentionAction() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EncodeDecodeJavaCodeIntentionAction : EncodeDecodeIntentionAction() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getFamilyName(): String = "Encode or decode Java string or identifier" @@ -20,7 +20,7 @@ internal class EncodeDecodeJavaCodeIntentionAction : EncodeDecodeIntentionAction return PsiJavaUtils.getTextIfStringValueOrIdentifier(psiElement) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/EscapeUnescapeJavaCodeIntentionAction.kt b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/EscapeUnescapeJavaCodeIntentionAction.kt index 1d972e99..0537628f 100644 --- a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/EscapeUnescapeJavaCodeIntentionAction.kt +++ b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/EscapeUnescapeJavaCodeIntentionAction.kt @@ -3,13 +3,13 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.tool.edito import com.intellij.openapi.editor.Editor import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention.EscapeUnescapeIntentionAction import dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.PsiJavaUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention.EscapeUnescapeIntentionAction -internal class EscapeUnescapeJavaCodeIntentionAction : EscapeUnescapeIntentionAction() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EscapeUnescapeJavaCodeIntentionAction : EscapeUnescapeIntentionAction() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getFamilyName(): String = "Escape or unescape Java string" @@ -20,7 +20,7 @@ internal class EscapeUnescapeJavaCodeIntentionAction : EscapeUnescapeIntentionAc return PsiJavaUtils.getTextIfStringValue(psiElement) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/TextCaseConverterJavaCodeIntentionAction.kt b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/TextCaseConverterJavaCodeIntentionAction.kt index 6a1bd748..719e4473 100644 --- a/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/TextCaseConverterJavaCodeIntentionAction.kt +++ b/modules/java-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/TextCaseConverterJavaCodeIntentionAction.kt @@ -3,13 +3,13 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.tool.edito import com.intellij.openapi.editor.Editor import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention.TextCaseConverterIntentionAction import dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.PsiJavaUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention.TextCaseConverterIntentionAction -internal class TextCaseConverterJavaCodeIntentionAction : TextCaseConverterIntentionAction() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class TextCaseConverterJavaCodeIntentionAction : TextCaseConverterIntentionAction() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getFamilyName(): String = "Convert text case of Java string or identifier" @@ -20,7 +20,7 @@ internal class TextCaseConverterJavaCodeIntentionAction : TextCaseConverterInten return PsiJavaUtils.getTextIfStringValueOrIdentifier(psiElement) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/java-dependent/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/JavaDependentDescriptionTest.kt b/modules/java-dependent/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/JavaDependentDescriptionTest.kt index f963617c..1c41b1d9 100644 --- a/modules/java-dependent/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/JavaDependentDescriptionTest.kt +++ b/modules/java-dependent/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/javadependent/tool/editor/intention/JavaDependentDescriptionTest.kt @@ -1,40 +1,27 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.tool.editor.intention -import DescriptionTest -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource - -class JavaDependentDescriptionTest : DescriptionTest() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionDescriptionHtmlFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionDescriptionHtmlFileExist(intentionActionSimpleClassName) - } - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionBeforeTemplateFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionBeforeTemplateFileExist(intentionActionSimpleClassName) - } - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionAfterTemplateFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionAfterTemplateFileExist(intentionActionSimpleClassName) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - @JvmStatic - fun intentionActionSimpleClassNames() = - intentionActionSimpleClassNames("dev.turingcomplete.intellijdevelopertoolsplugin.javadependent.tool.editor.intention") - } -} \ No newline at end of file +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.IntentionDescriptionTest +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.TestFactory + +class JavaDependentDescriptionTest : IntentionDescriptionTest() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @TestFactory + override fun `test intention action description HTML file exists`(): List = + super.`do test intention action description HTML file exists`() + + @TestFactory + override fun `test intention action before template file exists`(): List = + super.`do test intention action before template file exists`() + + @TestFactory + override fun `test intention action after template file exists`(): List = + super.`do test intention action after template file exists`() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/kotlin-dependent/build.gradle.kts b/modules/kotlin-dependent/build.gradle.kts index 9923163a..385780cb 100644 --- a/modules/kotlin-dependent/build.gradle.kts +++ b/modules/kotlin-dependent/build.gradle.kts @@ -2,12 +2,12 @@ dependencies { intellijPlatform { bundledPlugins("org.jetbrains.kotlin") } -} -dependencies { implementation(project(":common")) + implementation(project(":tools-editor")) + testImplementation(libs.assertj.core) testImplementation(libs.bundles.junit.implementation) testRuntimeOnly(libs.bundles.junit.runtime) testImplementation(testFixtures(project(":common"))) -} \ No newline at end of file +} diff --git a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/PsiKotlinUtils.kt b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/PsiKotlinUtils.kt index c942f40b..d65edd6b 100644 --- a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/PsiKotlinUtils.kt +++ b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/PsiKotlinUtils.kt @@ -8,13 +8,13 @@ import com.intellij.psi.util.elementType import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.getSelectedText import org.jetbrains.kotlin.lexer.KtTokens -internal object PsiKotlinUtils { - // -- Variables --------------------------------------------------------------------------------------------------- // +object PsiKotlinUtils { + // -- Variables ----------------------------------------------------------- // private val textElementKtTokens = setOf(KtTokens.REGULAR_STRING_PART, KtTokens.IDENTIFIER) - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun getTextFromStringValueOrIdentifier(e: AnActionEvent): Pair? { val psiFile = e.getData(CommonDataKeys.PSI_FILE) ?: return null @@ -30,8 +30,7 @@ internal object PsiKotlinUtils { fun getTextFromStringValueOrIdentifier(psiElement: PsiElement): String? { return if (textElementKtTokens.contains(psiElement.elementType)) { psiElement.text - } - else { + } else { null } } @@ -50,12 +49,11 @@ internal object PsiKotlinUtils { fun getTextFromStringValue(psiElement: PsiElement): String? { return if (psiElement.elementType == KtTokens.REGULAR_STRING_PART) { psiElement.text - } - else { + } else { null } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/EncodeDecodeKotlinCodeActionGroup.kt b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/EncodeDecodeKotlinCodeActionGroup.kt index 65eb264c..0922ab65 100644 --- a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/EncodeDecodeKotlinCodeActionGroup.kt +++ b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/EncodeDecodeKotlinCodeActionGroup.kt @@ -2,18 +2,18 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.tool.edi import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.util.TextRange -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action.EncodeDecodeActionGroup import dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.PsiKotlinUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action.EncodeDecodeActionGroup -internal class EncodeDecodeKotlinCodeActionGroup : EncodeDecodeActionGroup() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EncodeDecodeKotlinCodeActionGroup : EncodeDecodeActionGroup() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getSourceText(e: AnActionEvent): Pair? = PsiKotlinUtils.getTextFromStringValueOrIdentifier(e) - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/EscapeUnescapeKotlinCodeActionGroup.kt b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/EscapeUnescapeKotlinCodeActionGroup.kt index d8670620..52383fde 100644 --- a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/EscapeUnescapeKotlinCodeActionGroup.kt +++ b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/EscapeUnescapeKotlinCodeActionGroup.kt @@ -2,18 +2,18 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.tool.edi import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.util.TextRange -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action.EscapeUnescapeActionGroup import dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.PsiKotlinUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action.EscapeUnescapeActionGroup -internal class EscapeUnescapeKotlinCodeActionGroup : EscapeUnescapeActionGroup() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EscapeUnescapeKotlinCodeActionGroup : EscapeUnescapeActionGroup() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getSourceText(e: AnActionEvent): Pair? = PsiKotlinUtils.getTextFromStringValue(e) - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/TextCaseConverterKotlinCodeActionGroup.kt b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/TextCaseConverterKotlinCodeActionGroup.kt index 0fd02c72..aca420bc 100644 --- a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/TextCaseConverterKotlinCodeActionGroup.kt +++ b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/action/TextCaseConverterKotlinCodeActionGroup.kt @@ -2,18 +2,18 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.tool.edi import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.util.TextRange -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action.TextCaseConverterActionGroup import dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.PsiKotlinUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action.TextCaseConverterActionGroup -internal class TextCaseConverterKotlinCodeActionGroup : TextCaseConverterActionGroup() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class TextCaseConverterKotlinCodeActionGroup : TextCaseConverterActionGroup() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getSourceText(e: AnActionEvent): Pair? = PsiKotlinUtils.getTextFromStringValueOrIdentifier(e) - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/EncodeDecodeKotlinCodeIntentionAction.kt b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/EncodeDecodeKotlinCodeIntentionAction.kt index a2aaa5c4..bfda52a0 100644 --- a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/EncodeDecodeKotlinCodeIntentionAction.kt +++ b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/EncodeDecodeKotlinCodeIntentionAction.kt @@ -3,13 +3,13 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.tool.edi import com.intellij.openapi.editor.Editor import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention.EncodeDecodeIntentionAction import dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.PsiKotlinUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention.EncodeDecodeIntentionAction -internal class EncodeDecodeKotlinCodeIntentionAction : EncodeDecodeIntentionAction() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EncodeDecodeKotlinCodeIntentionAction : EncodeDecodeIntentionAction() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getFamilyName(): String = "Encode or decode Kotlin string or identifier" @@ -17,10 +17,12 @@ internal class EncodeDecodeKotlinCodeIntentionAction : EncodeDecodeIntentionActi override fun getSourceText(editor: Editor, file: PsiFile): Pair? { val psiElement = file.findElementAt(editor.caretModel.offset) ?: return null - return PsiKotlinUtils.getTextFromStringValueOrIdentifier(psiElement)?.let { it to psiElement.textRange } + return PsiKotlinUtils.getTextFromStringValueOrIdentifier(psiElement)?.let { + it to psiElement.textRange + } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/EscapeUnescapeKotlinCodeIntentionAction.kt b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/EscapeUnescapeKotlinCodeIntentionAction.kt index 5d122e74..c1f9d157 100644 --- a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/EscapeUnescapeKotlinCodeIntentionAction.kt +++ b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/EscapeUnescapeKotlinCodeIntentionAction.kt @@ -3,13 +3,13 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.tool.edi import com.intellij.openapi.editor.Editor import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention.EncodeDecodeIntentionAction import dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.PsiKotlinUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention.EncodeDecodeIntentionAction -internal class EscapeUnescapeKotlinCodeIntentionAction : EncodeDecodeIntentionAction() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EscapeUnescapeKotlinCodeIntentionAction : EncodeDecodeIntentionAction() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getFamilyName(): String = "Escape or unescape Kotlin string" @@ -20,7 +20,7 @@ internal class EscapeUnescapeKotlinCodeIntentionAction : EncodeDecodeIntentionAc return PsiKotlinUtils.getTextFromStringValue(psiElement)?.let { it to psiElement.textRange } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/TextCaseConverterKotlinCodeIntentionAction.kt b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/TextCaseConverterKotlinCodeIntentionAction.kt index f15a6114..72338237 100644 --- a/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/TextCaseConverterKotlinCodeIntentionAction.kt +++ b/modules/kotlin-dependent/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/TextCaseConverterKotlinCodeIntentionAction.kt @@ -3,13 +3,13 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.tool.edi import com.intellij.openapi.editor.Editor import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention.TextCaseConverterIntentionAction import dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.PsiKotlinUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention.TextCaseConverterIntentionAction -internal class TextCaseConverterKotlinCodeIntentionAction : TextCaseConverterIntentionAction() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class TextCaseConverterKotlinCodeIntentionAction : TextCaseConverterIntentionAction() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getFamilyName(): String = "Convert text case of Kotlin string or identifier" @@ -17,10 +17,12 @@ internal class TextCaseConverterKotlinCodeIntentionAction : TextCaseConverterInt override fun getSourceText(editor: Editor, file: PsiFile): Pair? { val psiElement = file.findElementAt(editor.caretModel.offset) ?: return null - return PsiKotlinUtils.getTextFromStringValueOrIdentifier(psiElement)?.let { it to psiElement.textRange } + return PsiKotlinUtils.getTextFromStringValueOrIdentifier(psiElement)?.let { + it to psiElement.textRange + } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/kotlin-dependent/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/KotlinDependentDescriptionTest.kt b/modules/kotlin-dependent/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/KotlinDependentDescriptionTest.kt index f59ba9b6..87d78e33 100644 --- a/modules/kotlin-dependent/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/KotlinDependentDescriptionTest.kt +++ b/modules/kotlin-dependent/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/kotlindependent/tool/editor/intention/KotlinDependentDescriptionTest.kt @@ -1,40 +1,27 @@ package dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.tool.editor.intention -import DescriptionTest -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource - -class KotlinDependentDescriptionTest : DescriptionTest() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionDescriptionHtmlFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionDescriptionHtmlFileExist(intentionActionSimpleClassName) - } - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionBeforeTemplateFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionBeforeTemplateFileExist(intentionActionSimpleClassName) - } - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionAfterTemplateFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionAfterTemplateFileExist(intentionActionSimpleClassName) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - @JvmStatic - fun intentionActionSimpleClassNames() = - intentionActionSimpleClassNames("dev.turingcomplete.intellijdevelopertoolsplugin.kotlindependent.tool.editor.intention") - } -} \ No newline at end of file +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.IntentionDescriptionTest +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.TestFactory + +class KotlinDependentDescriptionTest : IntentionDescriptionTest() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @TestFactory + override fun `test intention action description HTML file exists`(): List = + super.`do test intention action description HTML file exists`() + + @TestFactory + override fun `test intention action before template file exists`(): List = + super.`do test intention action before template file exists`() + + @TestFactory + override fun `test intention action after template file exists`(): List = + super.`do test intention action after template file exists`() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/build.gradle.kts b/modules/settings/build.gradle.kts new file mode 100644 index 00000000..07b90ea4 --- /dev/null +++ b/modules/settings/build.gradle.kts @@ -0,0 +1,8 @@ +dependencies { + implementation(project(":common")) + + testImplementation(libs.assertj.core) + testImplementation(libs.bundles.junit.implementation) + testRuntimeOnly(libs.bundles.junit.runtime) + testImplementation(testFixtures(project(":common"))) +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperToolConfiguration.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfiguration.kt similarity index 55% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperToolConfiguration.kt rename to modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfiguration.kt index c50f7183..a9a7ad7d 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperToolConfiguration.kt +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfiguration.kt @@ -1,14 +1,13 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.main +package dev.turingcomplete.intellijdevelopertoolsplugin.settings import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty.Companion.RESET_CHANGE_ID -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsInstanceSettings.Companion.assertPersistableType +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings.Companion.assertPersistableType import java.math.BigDecimal -import java.util.* +import java.util.UUID import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import kotlin.reflect.safeCast @@ -16,39 +15,41 @@ import kotlin.reflect.safeCast class DeveloperToolConfiguration( var name: String, val id: UUID, - val persistentProperties: Map + val persistentProperties: Map, ) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // - internal val properties = ConcurrentHashMap() + val properties = ConcurrentHashMap() // If false, the `DeveloperToolFactory` never created a `DeveloperTool` // instance that could have register the current properties. // In this case, the `persistentProperties` need to be persisted again. - internal var wasConsumedByDeveloperTool = false + var wasConsumedByDeveloperTool = false private val changeListeners = CopyOnWriteArrayList() private val resetListeners = CopyOnWriteArrayList() var isResetting = false - internal set + private set - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // fun register( key: String, defaultValue: T, propertyType: PropertyType = PropertyType.CONFIGURATION, - example: T? = null + example: T? = null, ): ValueProperty = - properties[key]?.let { reuseExistingProperty(it) } ?: createNewProperty(defaultValue, propertyType, key, createExampleProvider(example)) + properties[key]?.let { reuseExistingProperty(it) } + ?: createNewProperty(defaultValue, propertyType, key, createExampleProvider(example)) fun registerWithExampleProvider( key: String, defaultValue: T, propertyType: PropertyType = PropertyType.CONFIGURATION, - example: (() -> T)? = null + example: (() -> T)? = null, ): ValueProperty = - properties[key]?.let { reuseExistingProperty(it) } ?: createNewProperty(defaultValue, propertyType, key, example) + properties[key]?.let { reuseExistingProperty(it) } + ?: createNewProperty(defaultValue, propertyType, key, example) fun addChangeListener(parentDisposable: Disposable, changeListener: ChangeListener) { changeListeners.add(changeListener) @@ -70,38 +71,39 @@ class DeveloperToolConfiguration( fun reset( type: PropertyType? = null, - loadExamples: Boolean = DeveloperToolsApplicationSettings.instance.loadExamples + loadExamples: Boolean = generalSettings.loadExamples.get(), ) { isResetting = true try { - properties.filter { type == null || it.value.type == type } + properties + .filter { type == null || it.value.type == type } .forEach { (_, property) -> property.reset(loadExamples) fireConfigurationChanged(property.reference) } resetListeners.forEach { it.configurationReset() } - } - finally { + } finally { isResetting = false } } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun createExampleProvider(example: T?) = if (example != null) { { example } - } - else { + } else { null } private fun reuseExistingProperty(property: PropertyContainer): ValueProperty { - if ((property.type == PropertyType.INPUT && !DeveloperToolsApplicationSettings.instance.saveInputs) - || (property.type == PropertyType.CONFIGURATION && !DeveloperToolsApplicationSettings.instance.saveConfigurations) - || (property.type == PropertyType.SENSITIVE && !DeveloperToolsApplicationSettings.instance.saveSensitiveInputs) + if ( + (property.type == PropertyType.INPUT && !generalSettings.saveInputs.get()) || + (property.type == PropertyType.CONFIGURATION && + !generalSettings.saveConfigurations.get()) || + (property.type == PropertyType.SENSITIVE && !generalSettings.saveSensitiveInputs.get()) ) { - property.reset(DeveloperToolsApplicationSettings.instance.loadExamples) + property.reset(generalSettings.loadExamples.get()) } @Suppress("UNCHECKED_CAST") @@ -112,23 +114,25 @@ class DeveloperToolConfiguration( defaultValue: T, propertyType: PropertyType, key: String, - example: (() -> T)? + example: (() -> T)?, ): ValueProperty { val type = assertPersistableType(defaultValue::class) val existingPropertyValue = persistentProperties[key]?.value - val initialValue: T = type.safeCast(existingPropertyValue) ?: let { - if (DeveloperToolsApplicationSettings.instance.loadExamples && example != null) example() else defaultValue - } - val valueProperty = ValueProperty(initialValue).apply { - afterChangeConsumeEvent(null, handlePropertyChange(key)) - } - properties[key] = PropertyContainer( - key = key, - reference = valueProperty, - defaultValue = defaultValue, - example = example, - type = propertyType - ) + val initialValue: T = + type.safeCast(existingPropertyValue) + ?: let { + if (generalSettings.loadExamples.get() && example != null) example() else defaultValue + } + val valueProperty = + ValueProperty(initialValue).apply { afterChangeConsumeEvent(null, handlePropertyChange(key)) } + properties[key] = + PropertyContainer( + key = key, + reference = valueProperty, + defaultValue = defaultValue, + example = example, + type = propertyType, + ) return valueProperty } @@ -136,52 +140,54 @@ class DeveloperToolConfiguration( changeListeners.forEach { it.configurationChanged(property) } } - private fun handlePropertyChange(key: String): (ValueProperty.ChangeEvent) -> Unit = { event -> - val newValue = event.newValue - if (event.oldValue != newValue) { - properties[key]?.let { property -> - fireConfigurationChanged(property.reference) - } ?: error("Unknown property: $key") + private fun handlePropertyChange(key: String): (ValueProperty.ChangeEvent) -> Unit = + { event -> + val newValue = event.newValue + if (event.oldValue != newValue) { + properties[key]?.let { property -> fireConfigurationChanged(property.reference) } + ?: error("Unknown property: $key") + } } - } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - internal data class PropertyContainer( + data class PropertyContainer( val key: String, val reference: ValueProperty, val defaultValue: Any, val example: (() -> Any)?, - val type: PropertyType + val type: PropertyType, ) { fun reset(loadExamples: Boolean) { val value = if (example != null && loadExamples) example.invoke() else defaultValue - reference.setWithUncheckedCast(value, RESET_CHANGE_ID) + reference.setWithUncheckedCast(value, ValueProperty.RESET_CHANGE_ID) } fun valueWasChanged(): Boolean { val value = reference.get() - val b = if (defaultValue is BigDecimal) { - defaultValue.compareTo(value as BigDecimal) != 0 && example?.invoke()?.uncheckedCastTo()?.compareTo(value)?.equals(0)?.not() ?: true - } - else { - defaultValue != value && example?.invoke()?.equals(value)?.not() ?: true - } + val b = + if (defaultValue is BigDecimal) { + defaultValue.compareTo(value as BigDecimal) != 0 && + example?.invoke()?.uncheckedCastTo()?.compareTo(value)?.equals(0)?.not() != + false + } else { + defaultValue != value && example?.invoke()?.equals(value)?.not() != false + } return b } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // enum class PropertyType { CONFIGURATION, INPUT, - SENSITIVE + SENSITIVE, } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // @FunctionalInterface fun interface ChangeListener { @@ -189,7 +195,7 @@ class DeveloperToolConfiguration( fun configurationChanged(property: ValueProperty) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // @FunctionalInterface fun interface ResetListener { @@ -197,9 +203,9 @@ class DeveloperToolConfiguration( fun configurationReset() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // data class PersistentProperty(val key: String, val value: Any, val type: PropertyType) - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfigurationEnumPropertyTypeEp.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfigurationEnumPropertyTypeEp.kt new file mode 100644 index 00000000..f8b63591 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfigurationEnumPropertyTypeEp.kt @@ -0,0 +1,42 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.util.xmlb.annotations.Attribute +import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo +import kotlin.reflect.KClass + +class DeveloperToolConfigurationEnumPropertyTypeEp> : + DeveloperToolConfigurationPropertyType { + // -- Properties ---------------------------------------------------------- // + + @Attribute("id") @RequiredElement override lateinit var id: String + + @Attribute("type") @RequiredElement lateinit var type: String + + @Attribute("legacyId") override var legacyId: String? = null + + override val typeClass: KClass + get() = Class.forName(type).kotlin.uncheckedCastTo>() + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun fromPersistent(persistentValue: String): T { + return java.lang.Enum.valueOf(typeClass.java, persistentValue) + } + + override fun toPersistent(value: Any): String = value.uncheckedCastTo>().name + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + val epName: ExtensionPointName> = + ExtensionPointName.create( + "dev.turingcomplete.intellijdevelopertoolsplugins.developerToolConfigurationEnumPropertyType" + ) + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfigurationPropertyType.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfigurationPropertyType.kt new file mode 100644 index 00000000..c4655060 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolConfigurationPropertyType.kt @@ -0,0 +1,38 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import kotlin.reflect.KClass + +interface DeveloperToolConfigurationPropertyType { + // -- Properties ---------------------------------------------------------- // + + val id: String + val typeClass: KClass + val legacyId: String? + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + fun fromPersistent(persistentValue: String): T + + fun toPersistent(value: Any): String + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + data class SimplePropertyType( + override val id: String, + override val typeClass: KClass, + val doFromPersistent: (String) -> T, + val doToPersistent: (T) -> String, + override val legacyId: String? = null, + ) : DeveloperToolConfigurationPropertyType { + + override fun fromPersistent(persistentValue: String): T = doFromPersistent(persistentValue) + + @Suppress("UNCHECKED_CAST") + override fun toPersistent(value: Any): String = doToPersistent(value as T) + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsApplicationSettings.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsApplicationSettings.kt new file mode 100644 index 00000000..27cfe302 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsApplicationSettings.kt @@ -0,0 +1,142 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.SettingsCategory +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.diagnostic.logger +import dev.turingcomplete.intellijdevelopertoolsplugin.common.CryptoUtils.registerBouncyCastleProvider +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.ActionHandlingInstance.DIALOG +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.ActionHandlingInstance.TOOL_WINDOW +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsHandler.settingsContainer +import org.jdom.Element + +@Service +@State( + name = "DeveloperToolsApplicationSettingsV1", + storages = [Storage("developer-tools.xml")], + category = SettingsCategory.TOOLS, +) +class DeveloperToolsApplicationSettings : PersistentStateComponent { + // -- Properties ---------------------------------------------------------- // + + val generalSettings: GeneralSettings by lazy { SettingsHandler.create(GeneralSettings::class) } + val internalSettings: InternalSettings by lazy { SettingsHandler.create(InternalSettings::class) } + val jsonHandling: JsonHandlingSettings by lazy { + SettingsHandler.create(JsonHandlingSettings::class) + } + + private val allSettings = listOf(generalSettings, internalSettings, jsonHandling) + + // -- Initialization ------------------------------------------------------ // + + init { + try { + registerBouncyCastleProvider() + } catch (e: Exception) { + log.debug("Can't load BouncyCastleProvider", e) + } + } + + // -- Exported Methods ---------------------------------------------------- // + + override fun getState(): Element { + val root = Element("Root") + + allSettings.forEach { settings -> + val settingsContainer = settings.settingsContainer() + val settingsElement = Element(settingsContainer.kclass.simpleName) + + settingsContainer.settingProperties + .filter { it.value.isModified() } + .forEach { settingName, settingProperty -> + settingProperty.toPersistent()?.let { settingsElement.setAttribute(settingName, it) } + } + + root.addContent(settingsElement) + } + + return root + } + + override fun loadState(state: Element) { + applyLegacy(state) + + state.children.forEach { settingsElement -> + val settings = + allSettings + .map { it.settingsContainer() } + .firstOrNull { settingsContainer -> + settingsContainer.kclass.simpleName == settingsElement.name + } + if (settings == null) { + log.warn("Can't find settings class: ${settingsElement.name}") + return@forEach + } + + settings.settingProperties.forEach { (settingName, property) -> + settingsElement.getAttributeValue(settingName)?.let { property.fromPersistent(it) } + } + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun applyLegacy(state: Element) { + if (state.attributes.none { it.name != "name" }) { + return + } + + val generalSettingsElement = Element(GeneralSettings::class.simpleName) + val internalSettingsElement = Element(InternalSettings::class.simpleName) + + state.attributes.iterator().run { + while (hasNext()) { + val attribute = next() + val transformedValue: String? = + when (attribute.name) { + "selectedActionHandlingInstance" -> + when (attribute.value) { + "Tool Window" -> TOOL_WINDOW.name + "Dialog" -> DIALOG.name + else -> null + } + else -> attribute.value + } + if (transformedValue != null) { + val targetSettingsElement = generalSettingsElement + targetSettingsElement.setAttribute(attribute.name, transformedValue) + } + remove() + } + } + + state.addContent(generalSettingsElement) + state.addContent(internalSettingsElement) + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val log = logger() + + val instance: DeveloperToolsApplicationSettings + get() = + ApplicationManager.getApplication() + .getService(DeveloperToolsApplicationSettings::class.java) + + val generalSettings: GeneralSettings + get() = instance.generalSettings + + val internalSettings: InternalSettings + get() = instance.internalSettings + + val jsonHandling: JsonHandlingSettings + get() = instance.jsonHandling + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsDialogSettings.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsDialogSettings.kt new file mode 100644 index 00000000..99ce5a14 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsDialogSettings.kt @@ -0,0 +1,29 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.SettingsCategory +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage + +@Service +@State( + name = "DeveloperToolsDialogSettingsV1", + storages = [Storage("developer-tools.xml")], + category = SettingsCategory.TOOLS, +) +class DeveloperToolsDialogSettings : DeveloperToolsInstanceSettings() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + val instance: DeveloperToolsDialogSettings + get() = + ApplicationManager.getApplication().getService(DeveloperToolsDialogSettings::class.java) + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsInstanceSettings.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsInstanceSettings.kt new file mode 100644 index 00000000..b7be7426 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsInstanceSettings.kt @@ -0,0 +1,357 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.diagnostic.logger +import com.intellij.ui.JBColor +import com.intellij.util.xmlb.Converter +import com.intellij.util.xmlb.annotations.Attribute +import com.intellij.util.xmlb.annotations.Tag +import com.intellij.util.xmlb.annotations.XCollection +import com.intellij.util.xmlb.annotations.XCollection.Style.v2 +import com.jetbrains.rd.util.UUID +import dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginInfo +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginInfo.PluginVersion.Companion.toPluginVersion +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PersistentProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.SENSITIVE +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfigurationPropertyType.SimplePropertyType +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings.InstanceState +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettingsLegacy.applyConfigurationPropertyKeyLegacies +import java.math.BigDecimal +import java.util.Locale +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.reflect.KClass + +abstract class DeveloperToolsInstanceSettings : PersistentStateComponent { + // -- Properties ---------------------------------------------------------- // + + private val developerToolsConfigurations = + ConcurrentHashMap>() + + val lastSelectedContentNodeId: ValueProperty = ValueProperty(null) + var expandedGroupNodeIds: MutableSet? = null + private set + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + fun setExpandedGroupNodeIds(expandedGroupNodeIds: Set) { + this.expandedGroupNodeIds = expandedGroupNodeIds.toMutableSet() + } + + fun getDeveloperToolConfigurations(developerToolId: String): List = + developerToolsConfigurations[developerToolId]?.toList() ?: emptyList() + + fun createDeveloperToolConfiguration(developerToolId: String): DeveloperToolConfiguration { + val newDeveloperToolConfiguration = + DeveloperToolConfiguration( + name = "Workbench", + id = UUID.randomUUID(), + persistentProperties = emptyMap(), + ) + developerToolsConfigurations.compute(developerToolId) { _, developerToolConfigurations -> + (developerToolConfigurations ?: CopyOnWriteArrayList()).also { + it.add(newDeveloperToolConfiguration) + } + } + return newDeveloperToolConfiguration + } + + fun removeDeveloperToolConfiguration( + developerToolId: String, + developerToolConfiguration: DeveloperToolConfiguration, + ) { + developerToolsConfigurations[developerToolId]?.remove(developerToolConfiguration) + } + + override fun getState(): InstanceState { + val stateDeveloperToolsConfigurations = + developerToolsConfigurations + .asSequence() + .flatMap { (developerToolId, developerToolConfigurations) -> + developerToolConfigurations.mapNotNull { + createDeveloperToolConfigurationState(developerToolId, it) + } + } + .toList() + + return InstanceState( + pluginVersion = PluginInfo.pluginVersion.toString(), + developerToolsConfigurations = stateDeveloperToolsConfigurations, + lastSelectedContentNodeId = lastSelectedContentNodeId.get(), + expandedGroupNodeIds = expandedGroupNodeIds?.toList(), + ) + } + + override fun loadState(state: InstanceState) { + lastSelectedContentNodeId.set(state.lastSelectedContentNodeId) + setExpandedGroupNodeIds(state.expandedGroupNodeIds?.toSet() ?: emptySet()) + + val statePluginVersion: PluginInfo.PluginVersion? = state.pluginVersion?.toPluginVersion() + + developerToolsConfigurations.clear() + state.developerToolsConfigurations + ?.filter { + it.developerToolId != null && it.id != null && it.name != null && it.properties != null + } + ?.forEach { developerToolsConfigurationState -> + val developerToolConfiguration = + DeveloperToolConfiguration( + name = developerToolsConfigurationState.name!!, + id = UUID.fromString(developerToolsConfigurationState.id!!), + persistentProperties = + developerToolsConfigurationState.properties + ?.asSequence() + // The `type` property can be `null` in cases in which a type was + // removed from the `PropertyType` enum. + ?.filter { it.key != null && it.type != null && it.value != null } + ?.filter { shouldSavePropertyType(it.type!!) } + ?.map { + val propertyKey = + applyConfigurationPropertyKeyLegacies( + statePluginVersion = statePluginVersion, + developerToolId = developerToolsConfigurationState.developerToolId!!, + propertyKey = it.key!!, + ) + propertyKey to PersistentProperty(propertyKey, it.value!!, it.type!!) + } + ?.toMap() ?: emptyMap(), + ) + + developerToolsConfigurations.compute(developerToolsConfigurationState.developerToolId!!) { + _, + developerToolConfigurations -> + (developerToolConfigurations ?: CopyOnWriteArrayList()).also { + it.add(developerToolConfiguration) + } + } + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun shouldSavePropertyType(propertyType: PropertyType): Boolean = + when (propertyType) { + CONFIGURATION -> generalSettings.saveConfigurations.get() + INPUT -> generalSettings.saveInputs.get() + SENSITIVE -> generalSettings.saveSensitiveInputs.get() + } + + private fun createDeveloperToolConfigurationState( + developerToolId: String, + developerToolConfiguration: DeveloperToolConfiguration, + ): DeveloperToolConfigurationState? { + val properties = + if (developerToolConfiguration.wasConsumedByDeveloperTool) { + developerToolConfiguration.properties + .filter { (_, property) -> property.valueWasChanged() } + .map { PersistentProperty(key = it.key, value = it.value.reference.get(), it.value.type) } + } else { + developerToolConfiguration.persistentProperties.values + } + if (properties.isEmpty()) { + return null + } + + return DeveloperToolConfigurationState( + developerToolId = developerToolId, + id = developerToolConfiguration.id.toString(), + name = developerToolConfiguration.name, + properties = + properties + .filter { shouldSavePropertyType(it.type) } + .map { + DeveloperToolConfigurationProperty(key = it.key, value = it.value, type = it.type) + }, + ) + } + + // -- Inner Type ---------------------------------------------------------- // + + open class InstanceState( + @get:Attribute("pluginVersion") var pluginVersion: String? = null, + @get:XCollection(style = v2, elementName = "developerToolsConfigurations") + var developerToolsConfigurations: List? = null, + @get:Attribute("lastSelectedContentNodeId") var lastSelectedContentNodeId: String? = null, + @get:XCollection(style = v2, elementName = "expandedGroupNodeId") + var expandedGroupNodeIds: List? = null, + ) + + // -- Inner Type ---------------------------------------------------------- // + + @Tag(value = "developerToolConfiguration") + data class DeveloperToolConfigurationState( + @get:Attribute("developerToolId") var developerToolId: String? = null, + @get:Attribute("id") var id: String? = null, + @get:Attribute("name") var name: String? = null, + @get:XCollection(style = v2, elementName = "properties") + var properties: List? = null, + ) + + // -- Inner Type ---------------------------------------------------------- // + + @Tag(value = "property") + data class DeveloperToolConfigurationProperty( + @get:Attribute("key") var key: String? = null, + @get:Attribute("value", converter = StatePropertyValueConverter::class) var value: Any? = null, + @get:Attribute("type", converter = PropertyTypeConverter::class) var type: PropertyType? = null, + ) + + // -- Inner Type ---------------------------------------------------------- // + + /** Using a dedicated converter to handle removed values from the [PropertyType]. */ + class PropertyTypeConverter : Converter() { + + override fun fromString(value: String): PropertyType? { + return PropertyType.entries.firstOrNull { it.name == value } + } + + override fun toString(value: PropertyType): String { + return value.name + } + } + + // -- Inner Type ---------------------------------------------------------- // + + class StatePropertyValueConverter : Converter() { + + override fun toString(value: Any): String { + val configurationPropertyType = + configurationConfigurationPropertyTypes[value::class] + ?: error("Unsupported configuration property type: ${value::class.qualifiedName}") + val name = configurationPropertyType.id + val persistedValue = configurationPropertyType.toPersistent(value) + return "$name$PROPERTY_TYPE_VALUE_DELIMITER$persistedValue" + } + + /** + * If the value can't be restored, this method must return null. All properties with null values + * will be filtered in [DeveloperToolsInstanceSettings.loadState]. + */ + override fun fromString(value: String): Any? { + return try { + val valueAndType = value.split(PROPERTY_TYPE_VALUE_DELIMITER, limit = 2) + check(valueAndType.size == 2) { "Malformed serialized value: $value" } + val valueType = valueAndType[0] + val actualValue = valueAndType[1] + val configurationPropertyType = + configurationPropertyTypesByNamesAndLegacyValueTypes[valueType] + return if (configurationPropertyType == null) { + log.warn("Missing property type: $valueType") + null + } else { + configurationPropertyType.fromPersistent(actualValue) + } + } catch (e: Exception) { + log.warn("Failed to load configuration property of value: $value", e) + null + } + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val log = logger() + + private const val PROPERTY_TYPE_VALUE_DELIMITER = "|" + + fun assertPersistableType(type: KClass): KClass { + check(configurationConfigurationPropertyTypes.contains(type)) { + "Unsupported configuration property type: ${type.qualifiedName}" + } + return type + } + + private val configurationConfigurationPropertyTypes: + Map, DeveloperToolConfigurationPropertyType<*>> by lazy { + builtInConfigurationPropertyTypes + loadEnumConfigurationPropertyTypes() + } + + val configurationPropertyTypesByNamesAndLegacyValueTypes: + Map> by lazy { + configurationConfigurationPropertyTypes.values + .flatMap { it -> + val propertyTypes = mutableListOf(it.id to it) + it.legacyId?.let { legacyName -> propertyTypes.add(legacyName to it) } + return@flatMap propertyTypes + } + .toMap() + } + + private fun loadEnumConfigurationPropertyTypes(): + Map, DeveloperToolConfigurationPropertyType<*>> = + DeveloperToolConfigurationEnumPropertyTypeEp.epName.extensions.asSequence().associateBy { + it.typeClass + } + + val builtInConfigurationPropertyTypes: + Map, DeveloperToolConfigurationPropertyType<*>> = + listOf( + SimplePropertyType( + id = Boolean::class.qualifiedName!!, + typeClass = Boolean::class, + doFromPersistent = { it.toBoolean() }, + doToPersistent = { it.toString() }, + ), + SimplePropertyType( + id = Int::class.qualifiedName!!, + typeClass = Int::class, + doFromPersistent = { it.toInt() }, + doToPersistent = { it.toString() }, + ), + SimplePropertyType( + id = Long::class.qualifiedName!!, + typeClass = Long::class, + doFromPersistent = { it.toLong() }, + doToPersistent = { it.toString() }, + ), + SimplePropertyType( + id = Float::class.qualifiedName!!, + typeClass = Float::class, + doFromPersistent = { it.toFloat() }, + doToPersistent = { it.toString() }, + ), + SimplePropertyType( + id = Double::class.qualifiedName!!, + typeClass = Double::class, + doFromPersistent = { it.toDouble() }, + doToPersistent = { it.toString() }, + ), + SimplePropertyType( + id = String::class.qualifiedName!!, + typeClass = String::class, + doFromPersistent = { it }, + doToPersistent = { it }, + ), + SimplePropertyType( + id = JBColor::class.qualifiedName!!, + typeClass = JBColor::class, + doFromPersistent = { JBColor(it.toInt(), it.toInt()) }, + doToPersistent = { it.rgb.toString() }, + ), + SimplePropertyType( + id = LocaleContainer::class.qualifiedName!!, + typeClass = LocaleContainer::class, + doFromPersistent = { LocaleContainer(Locale.forLanguageTag(it)) }, + doToPersistent = { it.locale.toLanguageTag() }, + legacyId = + "dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer", + ), + SimplePropertyType( + id = BigDecimal::class.qualifiedName!!, + typeClass = BigDecimal::class, + doFromPersistent = { BigDecimal(it) }, + doToPersistent = { it.toString() }, + ), + ) + .associateBy { it.typeClass } + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsInstanceSettingsLegacy.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsInstanceSettingsLegacy.kt new file mode 100644 index 00000000..8c5a8c99 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsInstanceSettingsLegacy.kt @@ -0,0 +1,118 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginInfo +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettingsLegacy.RenamedEditorPropertiesFrom630To640.renamedEditorIds +import java.util.regex.Pattern + +object DeveloperToolsInstanceSettingsLegacy { + // -- Properties ---------------------------------------------------------- // + + private val pluginVersion640 = PluginInfo.PluginVersion(6, 4, 0) + + private val configurationPropertyKeyLegacies: List = + listOf(RenamedPropertiesFrom630To640, RenamedEditorPropertiesFrom630To640) + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun applyConfigurationPropertyKeyLegacies( + statePluginVersion: PluginInfo.PluginVersion?, + developerToolId: String, + propertyKey: String, + ): String = + configurationPropertyKeyLegacies + .filter { it.shouldBeApplied(statePluginVersion) } + .fold(propertyKey) { modifiedPropertyKey, legacy -> + legacy.applyLegacy(developerToolId, modifiedPropertyKey) + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + private interface ConfigurationPropertyKeyLegacy { + + fun shouldBeApplied(statePluginVersion: PluginInfo.PluginVersion?): Boolean + + fun applyLegacy(developerToolId: String, propertyKey: String): String + } + + // -- Inner Type ---------------------------------------------------------- // + + private object RenamedPropertiesFrom630To640 : ConfigurationPropertyKeyLegacy { + + val migratedTextTransformersToUndirectionalConverter = + setOf( + "hmac-transformer", + "text-filter", + "text-case-transformer", + "sql-formatting", + "hashing-transformer", + "text-sorting-transformer", + ) + + override fun shouldBeApplied(statePluginVersion: PluginInfo.PluginVersion?): Boolean = + statePluginVersion == null || statePluginVersion < pluginVersion640 + + /** + * Rename property `liveTransformation` to `liveConversion` of `TextTransformer`s that have been + * migrated to `UndirectionalConverter`. + */ + override fun applyLegacy(developerToolId: String, propertyKey: String): String = + if ( + propertyKey == "liveTransformation" && + migratedTextTransformersToUndirectionalConverter.contains(developerToolId) + ) { + "liveConversion" + } else { + propertyKey + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private object RenamedEditorPropertiesFrom630To640 : ConfigurationPropertyKeyLegacy { + val sourceInputToSource = "source-input" to "source" + val resultOutputToTarget = "source-input" to "target" + val renamedEditorIds = + mapOf( + "hmac-transformer" to mapOf(sourceInputToSource, resultOutputToTarget), + "text-filter" to mapOf(sourceInputToSource, resultOutputToTarget), + "text-case-transformer" to mapOf(sourceInputToSource, resultOutputToTarget), + "sql-formatting" to mapOf(sourceInputToSource, resultOutputToTarget), + "hashing-transformer" to mapOf(sourceInputToSource, resultOutputToTarget), + "text-sorting-transformer" to mapOf(sourceInputToSource, resultOutputToTarget), + "jwt-encoder-decoder" to + mapOf( + "jwt-encoder-decoder-encoded" to "encoded", + "jwt-encoder-decoder-header" to "header", + "jwt-encoder-decoder-payload" to "payload", + ), + ) + val editorPropertySuffixes = listOf("softWraps", "showSpecialCharacters", "showWhitespaces") + + override fun shouldBeApplied(statePluginVersion: PluginInfo.PluginVersion?): Boolean = + statePluginVersion == null || statePluginVersion < pluginVersion640 + + /** + * - Append infix `-editor-` + * - Replace renamed editor IDs [renamedEditorIds] + */ + override fun applyLegacy(developerToolId: String, propertyKey: String): String { + val editorPropertySuffix = editorPropertySuffixes.firstOrNull { propertyKey.endsWith("-$it") } + if (editorPropertySuffix == null) { + return propertyKey + } + + val matcher = + Pattern.compile("^$developerToolId-(?.+)-$editorPropertySuffix$") + .matcher(propertyKey) + return if (matcher.matches()) { + var editorId = matcher.group("editorId") + editorId = renamedEditorIds[developerToolId]?.get(editorId) ?: editorId + "${editorId}-editor-$editorPropertySuffix" + } else { + propertyKey + } + } + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsToolWindowSettings.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsToolWindowSettings.kt similarity index 53% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsToolWindowSettings.kt rename to modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsToolWindowSettings.kt index f820d128..cc0d805b 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsToolWindowSettings.kt +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsToolWindowSettings.kt @@ -1,36 +1,30 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings +package dev.turingcomplete.intellijdevelopertoolsplugin.settings -import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.Service import com.intellij.openapi.components.Service.Level.PROJECT import com.intellij.openapi.components.SettingsCategory import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.project.Project -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsInstanceSettings.InstanceState @State( name = "DeveloperToolsToolWindowSettingsV1", storages = [Storage("developer-tools.xml")], - category = SettingsCategory.TOOLS + category = SettingsCategory.TOOLS, ) @Service(PROJECT) -internal class DeveloperToolsToolWindowSettings : - DeveloperToolsInstanceSettings(), - PersistentStateComponent { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // +class DeveloperToolsToolWindowSettings : DeveloperToolsInstanceSettings() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { - /** - * Warning: The first access to this service will trigger the `loadState()`. - */ + /** Warning: The first access to this service will trigger the `loadState()`. */ fun getInstance(project: Project): DeveloperToolsToolWindowSettings = project.getService(DeveloperToolsToolWindowSettings::class.java) } -} \ No newline at end of file +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/GeneralSettings.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/GeneralSettings.kt new file mode 100644 index 00000000..a1e0ebb3 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/GeneralSettings.kt @@ -0,0 +1,180 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.intellij.CommonBundle +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.Companion.ACTION_HANDLING_GROUP_ID +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.Companion.ADVANCED_GROUP_ID +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.Companion.DEFAULT_EDITOR_SETTINGS_GROUP_ID +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.BooleanSettingProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.BooleanValue +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.EnumSettingProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.EnumValue +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.InternalSetting +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.Setting +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.Settings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.message.SettingsBundle + +@SettingsGroup( + id = DEFAULT_EDITOR_SETTINGS_GROUP_ID, + titleBundleKey = "general-settings.default-editor-settings-group.title", + order = 0, +) +@SettingsGroup( + id = ACTION_HANDLING_GROUP_ID, + titleBundleKey = "general-settings.action-handling-group.title", + descriptionBundleKey = "general-settings.action-handling-group.description", + order = 1, +) +@SettingsGroup( + id = ADVANCED_GROUP_ID, + titleBundleKey = "general-settings.advanced-group.title", + order = 2, +) +interface GeneralSettings : Settings { + // -- Properties ---------------------------------------------------------- // + + @Setting( + titleBundleKey = "general-settings.add-open-main-dialog-action-to-main-toolbar.title", + descriptionBundleKey = + "general-settings.add-open-main-dialog-action-to-main-toolbar.description", + order = 0, + ) + @BooleanValue(defaultValue = false) + val addOpenMainDialogActionToMainToolbar: BooleanSettingProperty + + @Setting(titleBundleKey = "general-settings.load-examples.title", order = 1) + @BooleanValue(defaultValue = true) + val loadExamples: BooleanSettingProperty + + @Setting(titleBundleKey = "general-settings.save-configurations.title", order = 2) + @BooleanValue(defaultValue = true) + val saveConfigurations: BooleanSettingProperty + + @Setting(titleBundleKey = "general-settings.save-inputs.title", order = 3) + @BooleanValue(defaultValue = true) + val saveInputs: BooleanSettingProperty + + @Setting( + titleBundleKey = "general-settings.save-sensitive-inputs.title", + descriptionBundleKey = "general-settings.save-sensitive-inputs.description", + order = 4, + ) + @BooleanValue(defaultValue = false) + val saveSensitiveInputs: BooleanSettingProperty + + @Setting( + titleBundleKey = "general-settings.editor-soft-wraps.title", + groupId = DEFAULT_EDITOR_SETTINGS_GROUP_ID, + order = 5, + ) + @BooleanValue(defaultValue = true) + val editorSoftWraps: BooleanSettingProperty + + @Setting( + titleBundleKey = "general-settings.editor-show-special-characters.title", + groupId = DEFAULT_EDITOR_SETTINGS_GROUP_ID, + order = 6, + ) + @BooleanValue(defaultValue = false) + val editorShowSpecialCharacters: BooleanSettingProperty + + @Setting( + titleBundleKey = "general-settings.editor-show-whitespaces.title", + groupId = DEFAULT_EDITOR_SETTINGS_GROUP_ID, + order = 7, + ) + @BooleanValue(defaultValue = false) + val editorShowWhitespaces: BooleanSettingProperty + + @Setting( + titleBundleKey = "general-settings.tools-menu-tree-show-group-nodes.title", + groupId = ADVANCED_GROUP_ID, + order = 8, + ) + @BooleanValue(defaultValue = false) + val toolsMenuTreeShowGroupNodes: BooleanSettingProperty + + @Setting( + titleBundleKey = "general-settings.tools-menu-tree-order-alphabetically.title", + groupId = ADVANCED_GROUP_ID, + order = 9, + ) + @BooleanValue(defaultValue = true) + val toolsMenuTreeOrderAlphabetically: BooleanSettingProperty + + @InternalSetting(groupId = ACTION_HANDLING_GROUP_ID) + @BooleanValue(defaultValue = true) + val autoDetectActionHandlingInstance: BooleanSettingProperty + + @InternalSetting(groupId = ACTION_HANDLING_GROUP_ID) + @EnumValue( + enumClass = ActionHandlingInstance::class, + defaultValueName = "TOOL_WINDOW", + displayTextProvider = ActionHandlingInstanceDisplayTextProvider::class, + ) + val selectedActionHandlingInstance: EnumSettingProperty + + @Setting( + titleBundleKey = "general-settings.show-internal-tools.title", + groupId = ADVANCED_GROUP_ID, + descriptionBundleKey = "general-settings.show-internal-tools.description", + order = 10, + ) + @BooleanValue(defaultValue = false) + val showInternalTools: BooleanSettingProperty + + @Setting( + titleBundleKey = "general-settings.hide-workbench-tabs-on-single-tab.title", + groupId = ADVANCED_GROUP_ID, + order = 11, + ) + @BooleanValue(defaultValue = true) + val hideWorkbenchTabsOnSingleTab: BooleanSettingProperty + + @Setting( + titleBundleKey = "general-settings.dialog-is-modal.title", + groupId = ADVANCED_GROUP_ID, + order = 12, + ) + @BooleanValue(defaultValue = false) + val dialogIsModal: BooleanSettingProperty + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + + // -- Inner Type ---------------------------------------------------------- // + + enum class ActionHandlingInstance(val displayText: String) { + + TOOL_WINDOW(SettingsBundle.message("general-settings.action-handling-instance.tool-window")), + DIALOG(SettingsBundle.message("general-settings.action-handling-instance.dialog")); + + override fun toString(): String = displayText + } + + // -- Inner Type ---------------------------------------------------------- // + + class ActionHandlingInstanceDisplayTextProvider : + EnumValue.DisplayTextProvider { + + override fun toDisplayText(value: ActionHandlingInstance): String = value.displayText + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val DEFAULT_EDITOR_SETTINGS_GROUP_ID = "defaultEditorSettings" + private const val ADVANCED_GROUP_ID = "advanced" + const val ACTION_HANDLING_GROUP_ID = "actionHandling" + + fun GeneralSettings.createSensitiveInputsHandlingToolTipText(): String? = + if (saveInputs.get() && !saveSensitiveInputs.get()) { + "This sensitive input field will be cleared after the application is closed.
" + + "You can deactivate this behavior in the ${CommonBundle.settingsTitle().lowercase()}." + } else { + null + } + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/GeneralSettingsConfigurable.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/GeneralSettingsConfigurable.kt new file mode 100644 index 00000000..42d6e1d1 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/GeneralSettingsConfigurable.kt @@ -0,0 +1,56 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import dev.turingcomplete.intellijdevelopertoolsplugin.common.not +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.Companion.ACTION_HANDLING_GROUP_ID +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.AnySettingProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsConfigurable +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.message.SettingsBundle + +class GeneralSettingsConfigurable : + SettingsConfigurable(settings = generalSettings) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? = + SettingsBundle.message("general-settings.title") + + override fun buildGroupSettingsUi( + panel: Panel, + group: SettingsGroup, + groupSettings: List, + ) { + when (group.id) { + ACTION_HANDLING_GROUP_ID -> { + with(panel) { + buttonsGroup { + row { + radioButton(SettingsBundle.message("general-settings.action-handling.auto-detect")) + .bindSelected(settings.autoDetectActionHandlingInstance) + + radioButton(SettingsBundle.message("general-settings.action-handling.selected")) + .bindSelected(settings.autoDetectActionHandlingInstance.not()) + .gap(RightGap.SMALL) + comboBox(GeneralSettings.ActionHandlingInstance.entries) + .bindItem(settings.selectedActionHandlingInstance) + .enabledIf(settings.autoDetectActionHandlingInstance.not()) + } + } + } + } + + else -> super.buildGroupSettingsUi(panel, group, groupSettings) + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/InternalSettings.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/InternalSettings.kt new file mode 100644 index 00000000..80d0cdaf --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/InternalSettings.kt @@ -0,0 +1,12 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.Settings + +interface InternalSettings : Settings { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/JsonHandlingSettings.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/JsonHandlingSettings.kt new file mode 100644 index 00000000..82866eb0 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/JsonHandlingSettings.kt @@ -0,0 +1,246 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.fasterxml.jackson.core.util.Separators +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.JsonHandlingSettings.Companion.READ_CGROUP_ID +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.JsonHandlingSettings.Companion.WRITE_GROUP_ID +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.BooleanSettingProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.BooleanValue +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.EnumSettingProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.EnumValue +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.IntSettingProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.IntValue +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.Setting +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.Settings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.message.SettingsBundle + +@SettingsGroup( + id = WRITE_GROUP_ID, + titleBundleKey = "json-handling-settings.write-group.title", + order = 0, +) +@SettingsGroup( + id = READ_CGROUP_ID, + titleBundleKey = "json-handling-settings.read-group.title", + order = 1, +) +interface JsonHandlingSettings : Settings { + // -- Properties ---------------------------------------------------------- // + + @Setting( + titleBundleKey = "json-handling-settings.write-quote-field-names.title", + groupId = WRITE_GROUP_ID, + order = 0, + ) + @BooleanValue(defaultValue = true) + val writeQuoteFieldNames: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.write-nan-as-strings.title", + groupId = WRITE_GROUP_ID, + order = 1, + ) + @BooleanValue(defaultValue = true) + val writeNanAsStrings: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.write-numbers-as-strings.title", + groupId = WRITE_GROUP_ID, + order = 2, + ) + @BooleanValue(defaultValue = false) + val writeNumbersAsStrings: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.write-escape-non-ascii.title", + groupId = WRITE_GROUP_ID, + order = 3, + ) + @BooleanValue(defaultValue = false) + val writeEscapeNonAscii: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.write-hex-uppercase.title", + groupId = WRITE_GROUP_ID, + order = 4, + ) + @BooleanValue(defaultValue = true) + val writeHexUpperCase: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.write-intention-spaces.title", + groupId = WRITE_GROUP_ID, + order = 5, + ) + @IntValue(defaultValue = 2, min = 0, max = 10) + val writeIntentionSpaces: IntSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.write-spacing-object-field-value.title", + groupId = WRITE_GROUP_ID, + order = 6, + ) + @EnumValue( + enumClass = Separators.Spacing::class, + defaultValueName = "AFTER", + displayTextProvider = SeparatorsSpacingEnumDisplayTextProvider::class, + ) + val writeSpacingObjectFieldValue: EnumSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.write-spacing-object-entry.title", + groupId = WRITE_GROUP_ID, + order = 7, + ) + @EnumValue( + enumClass = Separators.Spacing::class, + defaultValueName = "NONE", + displayTextProvider = SeparatorsSpacingEnumDisplayTextProvider::class, + ) + val writeSpacingObjectEntry: EnumSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.write-spacing-array-value.title", + groupId = WRITE_GROUP_ID, + order = 8, + ) + @EnumValue( + enumClass = Separators.Spacing::class, + defaultValueName = "NONE", + displayTextProvider = SeparatorsSpacingEnumDisplayTextProvider::class, + ) + val writeSpacingArrayValue: EnumSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-java-comments.title", + groupId = READ_CGROUP_ID, + order = 9, + ) + @BooleanValue(defaultValue = false) + val readAllowJavaComments: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-yaml-comments.title", + groupId = READ_CGROUP_ID, + order = 10, + ) + @BooleanValue(defaultValue = false) + val readAllowYamlComments: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-single-quotes.title", + groupId = READ_CGROUP_ID, + order = 11, + ) + @BooleanValue(defaultValue = false) + val readAllowSingleQuotes: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-unquoted-field-names.title", + groupId = READ_CGROUP_ID, + order = 12, + ) + @BooleanValue(defaultValue = false) + val readAllowUnquotedFieldNames: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-unescaped-control-characters.title", + groupId = READ_CGROUP_ID, + order = 13, + ) + @BooleanValue(defaultValue = false) + val readAllowUnescapedControlChars: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-backslash-escaping-any-character.title", + groupId = READ_CGROUP_ID, + order = 14, + ) + @BooleanValue(defaultValue = false) + val readAllowBackslashEscapingAnyCharacter: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-leading-zeros-for-numbers.title", + groupId = READ_CGROUP_ID, + order = 15, + ) + @BooleanValue(defaultValue = false) + val readAllowLeadingZerosForNumbers: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-leading-plus-sign-for-numbers.title", + groupId = READ_CGROUP_ID, + order = 16, + ) + @BooleanValue(defaultValue = false) + val readAllowLeadingPlusSignForNumbers: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-leading-decimal-point-for-numbers.title", + groupId = READ_CGROUP_ID, + order = 17, + ) + @BooleanValue(defaultValue = false) + val readAllowLeadingDecimalPointForNumbers: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-trailing-decimal-point-for-numbers.title", + groupId = READ_CGROUP_ID, + order = 18, + ) + @BooleanValue(defaultValue = false) + val readAllowTrailingDecimalPointForNumbers: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-non-numeric-numbers.title", + groupId = READ_CGROUP_ID, + order = 19, + ) + @BooleanValue(defaultValue = false) + val readAllowNonNumericNumbers: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-missing-values.title", + groupId = READ_CGROUP_ID, + order = 20, + ) + @BooleanValue(defaultValue = false) + val readAllowMissingValues: BooleanSettingProperty + + @Setting( + titleBundleKey = "json-handling-settings.read-allow-trailing-comma.title", + groupId = READ_CGROUP_ID, + order = 21, + ) + @BooleanValue(defaultValue = false) + val readAllowTrailingComma: BooleanSettingProperty + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + class SeparatorsSpacingEnumDisplayTextProvider : + EnumValue.DisplayTextProvider { + + override fun toDisplayText(value: Separators.Spacing): String = + when (value) { + Separators.Spacing.NONE -> + SettingsBundle.message("json-handling-settings.write-separators-spacing.none") + Separators.Spacing.BEFORE -> + SettingsBundle.message("json-handling-settings.write-separators-spacing.before") + Separators.Spacing.AFTER -> + SettingsBundle.message("json-handling-settings.write-separators-spacing.after") + Separators.Spacing.BOTH -> + SettingsBundle.message("json-handling-settings.write-separators-spacing.both") + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val WRITE_GROUP_ID = "write" + private const val READ_CGROUP_ID = "read" + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/JsonHandlingSettingsConfigurable.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/JsonHandlingSettingsConfigurable.kt new file mode 100644 index 00000000..ae52d308 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/JsonHandlingSettingsConfigurable.kt @@ -0,0 +1,20 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.intellij.openapi.util.NlsContexts +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.jsonHandling +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsConfigurable +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.message.SettingsBundle + +class JsonHandlingSettingsConfigurable : + SettingsConfigurable(settings = jsonHandling) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? = + SettingsBundle.message("json-handling-settings.title") + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/BooleanSettingProperty.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/BooleanSettingProperty.kt new file mode 100644 index 00000000..b0eb4ad6 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/BooleanSettingProperty.kt @@ -0,0 +1,35 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +@Suppress("UNCHECKED_CAST") +class BooleanSettingProperty( + descriptor: Descriptor?, + group: SettingsGroup, + settingValue: BooleanValue, +) : + SettingProperty( + descriptor = descriptor, + group = group, + settingValue = settingValue, + initialValue = settingValue.defaultValue, + ) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun toPersistent(): String? { + val value = get() + return if (value != settingValue.defaultValue) { + value.toString() + } else { + null + } + } + + override fun fromPersistent(value: String) { + set(value.toBoolean()) + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/BooleanValue.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/BooleanValue.kt new file mode 100644 index 00000000..377eda4e --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/BooleanValue.kt @@ -0,0 +1,6 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +@SettingValue +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class BooleanValue(val defaultValue: Boolean) diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/EnumSettingProperty.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/EnumSettingProperty.kt new file mode 100644 index 00000000..60331f9a --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/EnumSettingProperty.kt @@ -0,0 +1,48 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +import dev.turingcomplete.intellijdevelopertoolsplugin.common.findEnumValueByName +import dev.turingcomplete.intellijdevelopertoolsplugin.common.getEnumValueByNameOrThrow + +class EnumSettingProperty>( + descriptor: Descriptor?, + group: SettingsGroup, + settingValue: EnumValue, +) : + SettingProperty>( + descriptor = descriptor, + group = group, + settingValue = settingValue, + initialValue = getDefaultValue(settingValue), + ) { + // -- Properties ---------------------------------------------------------- // + + private val defaultValue = getDefaultValue(settingValue) + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun toPersistent(): String? { + val value = get() + return if (value != defaultValue) { + value.name + } else { + null + } + } + + override fun fromPersistent(value: String) { + settingValue.enumClass.findEnumValueByName(value)?.let { set(it) } + } + + fun getAllEnumValues(): List = settingValue.enumClass.java.enumConstants.toList() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private fun > getDefaultValue(settingValue: EnumValue): T = + settingValue.enumClass.getEnumValueByNameOrThrow(settingValue.defaultValueName) + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/EnumValue.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/EnumValue.kt new file mode 100644 index 00000000..79d0a579 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/EnumValue.kt @@ -0,0 +1,21 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +import kotlin.reflect.KClass + +@SettingValue +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class EnumValue>( + val enumClass: KClass, + val defaultValueName: String, + val displayTextProvider: KClass>, +) { + + interface DisplayTextProvider { + + fun toDisplayText(value: T): String + + @Suppress("UNCHECKED_CAST") + fun toDisplayTextUncheckedCast(value: Any): String = toDisplayText(value as T) + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/IntSettingProperty.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/IntSettingProperty.kt new file mode 100644 index 00000000..3955dff2 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/IntSettingProperty.kt @@ -0,0 +1,31 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +@Suppress("UNCHECKED_CAST") +class IntSettingProperty(descriptor: Descriptor?, group: SettingsGroup, settingValue: IntValue) : + SettingProperty( + descriptor = descriptor, + group = group, + settingValue = settingValue, + initialValue = settingValue.defaultValue, + ) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun toPersistent(): String? { + val value = get() + return if (value != settingValue.defaultValue) { + value.toString() + } else { + null + } + } + + override fun fromPersistent(value: String) { + set(value.toInt()) + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/IntValue.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/IntValue.kt new file mode 100644 index 00000000..e1259339 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/IntValue.kt @@ -0,0 +1,10 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +@SettingValue +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class IntValue( + val defaultValue: Int, + val min: Int = Int.MIN_VALUE, + val max: Int = Int.MAX_VALUE, +) diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/InternalSetting.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/InternalSetting.kt new file mode 100644 index 00000000..7879bde0 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/InternalSetting.kt @@ -0,0 +1,5 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class InternalSetting(val groupId: String = "") diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/Setting.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/Setting.kt new file mode 100644 index 00000000..cf143aa0 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/Setting.kt @@ -0,0 +1,13 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.message.SettingsBundle.SETTINGS_BUNDLE_ID +import org.jetbrains.annotations.PropertyKey + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class Setting( + @PropertyKey(resourceBundle = SETTINGS_BUNDLE_ID) val titleBundleKey: String, + @PropertyKey(resourceBundle = SETTINGS_BUNDLE_ID) val descriptionBundleKey: String = "", + val groupId: String = "", + val order: Int, +) diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingProperty.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingProperty.kt new file mode 100644 index 00000000..d9785b0d --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingProperty.kt @@ -0,0 +1,30 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty + +@Suppress("UNCHECKED_CAST") +sealed class SettingProperty( + val descriptor: Descriptor?, + val group: SettingsGroup, + val settingValue: U, + val initialValue: T, +) : ValueProperty(initialValue) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + abstract fun toPersistent(): String? + + abstract fun fromPersistent(value: String) + + fun isModified(): Boolean = initialValue != get() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + data class Descriptor(val title: String, val description: String?, val order: Int) + + // -- Companion Object ---------------------------------------------------- // +} + +typealias AnySettingProperty = SettingProperty diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingValue.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingValue.kt new file mode 100644 index 00000000..7e95cc9e --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingValue.kt @@ -0,0 +1,20 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +import kotlin.reflect.KAnnotatedElement +import kotlin.reflect.KClass +import kotlin.reflect.full.findAnnotations + +@Target(AnnotationTarget.ANNOTATION_CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class SettingValue { + + companion object { + + val settingsAnnotations = + setOf>(BooleanValue::class, IntValue::class, EnumValue::class) + + fun KAnnotatedElement.findAmbiguousSettingValueAnnotation(): Annotation? { + return settingsAnnotations.flatMap { this.findAnnotations(it) }.singleOrNull() + } + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/Settings.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/Settings.kt new file mode 100644 index 00000000..a4ebb96b --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/Settings.kt @@ -0,0 +1,16 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +interface Settings { + // -- Properties ---------------------------------------------------------- // + + val modificationsCounter: Int + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + fun > getSetting(settingsName: String): U + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsConfigurable.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsConfigurable.kt new file mode 100644 index 00000000..c2a4d207 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsConfigurable.kt @@ -0,0 +1,111 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +import com.intellij.openapi.options.Configurable +import com.intellij.ui.SimpleListCellRenderer +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.bindIntText +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.panel +import com.jetbrains.rd.generator.nova.GenerationSpec.Companion.nullIfEmpty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsGroup.Companion.isDefaultGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsHandler.settingsContainer +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.message.SettingsBundle +import javax.swing.JComponent +import kotlin.reflect.full.createInstance + +abstract class SettingsConfigurable(protected val settings: T) : Configurable { + // -- Properties ---------------------------------------------------------- // + + private val derivatedSettingsContainer = settings.settingsContainer().derivate() + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + final override fun createComponent(): JComponent? = panel { + derivatedSettingsContainer.settingProperties + .asSequence() + .filter { it.value.descriptor != null } + .groupBy({ it.value.group }, { it.value }) + .toSortedMap { firstGroup, secondGroup -> + (firstGroup?.order ?: 0).compareTo(secondGroup?.order ?: 0) + } + .forEach { group, settings -> + val sortedSettings = settings.sortedBy { it.descriptor!!.order } + if (group.isDefaultGroup()) { + sortedSettings.forEach { buildSettingUi(it) } + } else { + group(SettingsBundle.message(group.titleBundleKey)) { + group.descriptionBundleKey.nullIfEmpty()?.let { row { comment(it) } } + buildGroupSettingsUi(this, group, sortedSettings) + } + } + } + } + + protected open fun buildGroupSettingsUi( + panel: Panel, + group: SettingsGroup, + groupSettings: List, + ) { + groupSettings.forEach { panel.buildSettingUi(it) } + } + + final override fun isModified(): Boolean = derivatedSettingsContainer.isModified() + + final override fun reset() { + derivatedSettingsContainer.reset() + } + + final override fun apply() { + derivatedSettingsContainer.apply() + } + + protected fun Panel.buildSettingUi(settingProperty: AnySettingProperty) { + val descriptor = settingProperty.descriptor as SettingProperty.Descriptor + row { + when (settingProperty) { + is BooleanSettingProperty -> { + val checkBox = checkBox(descriptor.title).bindSelected(settingProperty) + descriptor.description?.let { checkBox.comment(it) } + } + + is IntSettingProperty -> { + val intRange = + IntRange( + start = settingProperty.settingValue.min, + endInclusive = settingProperty.settingValue.max, + ) + val intTextField = intTextField(intRange).bindIntText(settingProperty) + intTextField.label(descriptor.title) + descriptor.description?.let { intTextField.comment(it) } + } + + is EnumSettingProperty<*> -> { + val enumValueRenderer = + SimpleListCellRenderer.create { label, value, _ -> + label.text = + settingProperty.settingValue.displayTextProvider + .createInstance() + .toDisplayTextUncheckedCast(value) + } + val comboBox = + comboBox( + settingProperty.getAllEnumValues().uncheckedCastTo>(), + enumValueRenderer, + ) + .bindItem(settingProperty) + .label(descriptor.title) + descriptor.description?.let { comboBox.comment(it) } + } + } + } + .layout(RowLayout.PARENT_GRID) + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsGroup.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsGroup.kt new file mode 100644 index 00000000..8533b212 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsGroup.kt @@ -0,0 +1,23 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.message.SettingsBundle.SETTINGS_BUNDLE_ID +import org.jetbrains.annotations.PropertyKey + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@Repeatable +annotation class SettingsGroup( + val id: String, + @PropertyKey(resourceBundle = SETTINGS_BUNDLE_ID) val titleBundleKey: String, + @PropertyKey(resourceBundle = SETTINGS_BUNDLE_ID) val descriptionBundleKey: String = "", + val order: Int, +) { + + companion object { + + fun SettingsGroup.isDefaultGroup(): Boolean = this == defaultSettingsGroup + + val defaultSettingsGroup = + SettingsGroup(id = "defaultGroup", titleBundleKey = "", descriptionBundleKey = "", order = -1) + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsHandler.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsHandler.kt new file mode 100644 index 00000000..eda352ed --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/base/SettingsHandler.kt @@ -0,0 +1,153 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.base + +import com.jetbrains.rd.generator.nova.GenerationSpec.Companion.nullIfEmpty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingValue.Companion.findAmbiguousSettingValueAnnotation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsGroup.Companion.defaultSettingsGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.message.SettingsBundle +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import kotlin.reflect.KClass +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.findAnnotations +import kotlin.reflect.full.memberProperties + +object SettingsHandler { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + fun create(clazz: KClass): T { + @Suppress("UNCHECKED_CAST") + return Proxy.newProxyInstance( + clazz.java.classLoader, + arrayOf(clazz.java), + SettingsContainer(clazz), + ) as T + } + + fun T.settingsContainer(): SettingsContainer = + Proxy.getInvocationHandler(this).uncheckedCastTo>() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + open class SettingsContainer(val kclass: KClass) : InvocationHandler { + + val settingProperties: LinkedHashMap by lazy { + collectSettingsProperties() + } + + override fun invoke(proxy: Any, method: Method, args: Array?): Any? { + val methodName = method.name + val noArgs = args?.isEmpty() != false + return when { + methodName == "getModificationsCounter" && noArgs -> + settingProperties.map { it.value.modificationsCounter }.sum() + + methodName == "getSetting" && args?.size == 1 -> + settingProperties[args.first()] ?: error("Setting `${args.first()}` does not exist") + + methodName.startsWith("get") && noArgs -> { + val settingName = methodName.removePrefix("get").replaceFirstChar { it.lowercase() } + settingProperties[settingName] ?: error("Setting `$settingName` does not exist") + } + + else -> error("Unknown method: $methodName(${args?.joinToString() ?: ""})") + } + } + + fun derivate() = DerivatedSettingsContainer(kclass, this) + + private fun collectSettingsProperties(): LinkedHashMap { + val settingsGroups = kclass.findAnnotations().associateBy { it.id } + + fun String.getSettingsGroup(): SettingsGroup = + this.nullIfEmpty()?.let { settingsGroups[it] ?: error("Unknown settings group ID: $it") } + ?: defaultSettingsGroup + + return kclass.memberProperties + .filter { it.name != "modificationsCounter" } + .associateTo(LinkedHashMap()) { property -> + val setting = property.findAnnotation() + val internalSetting = property.findAnnotation() + val (descriptor, settingsGroup) = + if (setting != null && internalSetting == null) { + Pair( + SettingProperty.Descriptor( + title = SettingsBundle.message(setting.titleBundleKey), + description = + setting.descriptionBundleKey.nullIfEmpty()?.let { SettingsBundle.message(it) }, + order = setting.order, + ), + setting.groupId.getSettingsGroup(), + ) + } else if (setting == null && internalSetting != null) { + Pair(null, internalSetting.groupId.getSettingsGroup()) + } else { + error( + "Property `${property.name}` requires either ${Setting::class.simpleName} or ${InternalSetting::class.simpleName} annotation" + ) + } + + val settingValue = + property.findAmbiguousSettingValueAnnotation() + ?: error( + "Property `${property.name}` is missing an ambiguous settings value annotation" + ) + + val settingsProperty: AnySettingProperty = + when (settingValue) { + is IntValue -> + IntSettingProperty( + descriptor = descriptor, + group = settingsGroup, + settingValue = settingValue, + ) + + is BooleanValue -> + BooleanSettingProperty( + descriptor = descriptor, + group = settingsGroup, + settingValue = settingValue, + ) + + is EnumValue<*> -> + EnumSettingProperty( + descriptor = descriptor, + group = settingsGroup, + settingValue = settingValue, + ) + + else -> error("Unknown setting value annotation: $settingValue") + }.uncheckedCastTo() + + assert((property.returnType.classifier as KClass<*>) == settingsProperty::class) { + "Property `${property.name}` is not of type ${settingsProperty::class}" + } + + property.name to settingsProperty + } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + class DerivatedSettingsContainer( + clazz: KClass, + val parent: SettingsContainer, + ) : SettingsContainer(clazz) { + + fun isModified(): Boolean = + settingProperties.any { parent.settingProperties[it.key]!!.get() != it.value.get() } + + fun apply() { + settingProperties.forEach { parent.settingProperties[it.key]!!.set(it.value.get()) } + } + + fun reset() { + settingProperties.forEach { it.value.set(parent.settingProperties[it.key]!!.get()) } + } + } +} diff --git a/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/message/SettingsBundle.kt b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/message/SettingsBundle.kt new file mode 100644 index 00000000..f89239b7 --- /dev/null +++ b/modules/settings/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/message/SettingsBundle.kt @@ -0,0 +1,24 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.message + +import com.intellij.DynamicBundle +import org.jetbrains.annotations.PropertyKey + +object SettingsBundle { + // -- Properties ---------------------------------------------------------- // + + const val SETTINGS_BUNDLE_ID = "message.SettingsBundle" + + private val instance: DynamicBundle = + DynamicBundle(SettingsBundle::class.java, SETTINGS_BUNDLE_ID) + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun message( + @PropertyKey(resourceBundle = SETTINGS_BUNDLE_ID) key: String, + vararg params: Any, + ): String = instance.getMessage(key, *params) + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/modules/settings/src/main/resources/message/SettingsBundle.properties b/modules/settings/src/main/resources/message/SettingsBundle.properties new file mode 100644 index 00000000..4cc10ede --- /dev/null +++ b/modules/settings/src/main/resources/message/SettingsBundle.properties @@ -0,0 +1,54 @@ +general-settings.title=Developer Tools +general-settings.default-editor-settings-group.title=Default Editor Settings +general-settings.advanced-group.title=Advanced +general-settings.action-handling-group.title=Action Handling for Developer Tools +general-settings.action-handling-group.description=This plugin offers actions across various areas in IntelliJ, such as the editor or project file tree, to open developer tools. Use this setting to choose whether these actions should open in a dialog or a tool window. +general-settings.add-open-main-dialog-action-to-main-toolbar.title=Add 'Developer Tools' action to the main toolbar during startup +general-settings.add-open-main-dialog-action-to-main-toolbar.description=If the action was previously removed manually, the automatic mechanism will not restore it. To enable it again, manually add the 'Developer Tools' action back to the 'Main Toolbar Right' (or 'Main Toolbar' in the old UI). +general-settings.load-examples.title=Load examples +general-settings.save-configurations.title=Remember configurations +general-settings.save-inputs.title=Remember inputs +general-settings.save-sensitive-inputs.title=Remember sensitive inputs +general-settings.save-sensitive-inputs.description=Sensitive inputs will be stored in plaintext. +general-settings.editor-soft-wraps.title=Soft-wraps +general-settings.editor-show-special-characters.title=Show special characters +general-settings.editor-show-whitespaces.title=Show whitespaces +general-settings.tools-menu-tree-show-group-nodes.title=Show group nodes in the tools menu +general-settings.tools-menu-tree-order-alphabetically.title=Order tools alphabetically in the tools menu +general-settings.show-internal-tools.title=Show internal tools +general-settings.show-internal-tools.description=Enables additional developer tools that are only useful for special applications, such as supporting IntelliJ plugin development. +general-settings.hide-workbench-tabs-on-single-tab.title=Hide workbench tabs if there is only one tab +general-settings.dialog-is-modal.title=Dialog is modal and must be closed before continuing to work with IntelliJ +general-settings.action-handling-instance.tool-window=Tool Window +general-settings.action-handling-instance.dialog=Dialog +general-settings.action-handling.auto-detect=Auto detect +general-settings.action-handling.selected=Use: +json-handling-settings.title=JSON Handling +json-handling-settings.write-group.title=Write +json-handling-settings.read-group.title=Read +json-handling-settings.write-quote-field-names.title=Quote field names +json-handling-settings.write-nan-as-strings.title=NaN as strings +json-handling-settings.write-numbers-as-strings.title=Numbers as strings +json-handling-settings.write-escape-non-ascii.title=Escape non-ASCII +json-handling-settings.write-hex-uppercase.title=Hex uppercase +json-handling-settings.write-intention-spaces.title=Intention spaces: +json-handling-settings.write-separators-spacing.none=None +json-handling-settings.write-separators-spacing.before=Before +json-handling-settings.write-separators-spacing.after=After +json-handling-settings.write-separators-spacing.both=Both +json-handling-settings.write-spacing-object-field-value.title=Object field spacing: +json-handling-settings.write-spacing-object-entry.title=Object entry spacing: +json-handling-settings.write-spacing-array-value.title=Array value spacing: +json-handling-settings.read-allow-java-comments.title=Allow Java comments +json-handling-settings.read-allow-yaml-comments.title=Allow YAML comments +json-handling-settings.read-allow-single-quotes.title=Allow single quotes +json-handling-settings.read-allow-unquoted-field-names.title=Allow unquoted field names +json-handling-settings.read-allow-unescaped-control-characters.title=Allow unescaped control characters +json-handling-settings.read-allow-backslash-escaping-any-character.title=Allow backslash escaping any character +json-handling-settings.read-allow-leading-zeros-for-numbers.title=Allow leading zeros for numbers +json-handling-settings.read-allow-leading-plus-sign-for-numbers.title=Allow leading plus sign for numbers +json-handling-settings.read-allow-leading-decimal-point-for-numbers.title=Allow leading decimal point for numbers +json-handling-settings.read-allow-trailing-decimal-point-for-numbers.title=Allow trailing decimal point for numbers +json-handling-settings.read-allow-non-numeric-numbers.title=Allow non-numeric numbers +json-handling-settings.read-allow-missing-values.title=Allow missing values +json-handling-settings.read-allow-trailing-comma.title=Allow trailing comma diff --git a/modules/settings/src/main/resources/message/SettingsBundle_de.properties b/modules/settings/src/main/resources/message/SettingsBundle_de.properties new file mode 100644 index 00000000..6a819e94 --- /dev/null +++ b/modules/settings/src/main/resources/message/SettingsBundle_de.properties @@ -0,0 +1,54 @@ +general-settings.title=Entwickler-Tools +general-settings.default-editor-settings-group.title=Standard-Editor-Einstellungen +general-settings.advanced-group.title=Erweiterte Einstellungen +general-settings.action-handling-group.title=Aktionen für Entwicklertools +general-settings.action-handling-group.description=Dieses Plugin bietet Aktionen in verschiedenen Bereichen von IntelliJ, wie z. B. im Editor oder im Projektdateibaum, um Entwicklertools zu öffnen. Mit dieser Einstellung können Sie festlegen, ob diese Aktionen im Dialog oder im Tool-Fenster geöffnet werden sollen. +general-settings.add-open-main-dialog-action-to-main-toolbar.title=Füge die Aktion 'Entwickler-Tools' beim Start zur Hauptsymbolleiste hinzu +general-settings.add-open-main-dialog-action-to-main-toolbar.description=Falls die Aktion zuvor manuell entfernt wurde, wird sie nicht automatisch wiederhergestellt. Um sie erneut zu aktivieren, füge die Aktion 'Entwickler-Tools' manuell zur 'Hauptsymbolleiste rechts' (oder 'Hauptsymbolleiste' im alten UI) hinzu. +general-settings.load-examples.title=Beispiele laden +general-settings.save-configurations.title=Konfigurationen speichern +general-settings.save-inputs.title=Eingaben speichern +general-settings.save-sensitive-inputs.title=Sensitive Eingaben speichern +general-settings.save-sensitive-inputs.description=Sensitive Eingaben werden im Klartext gespeichert. +general-settings.editor-soft-wraps.title=Weiche Umbrüche +general-settings.editor-show-special-characters.title=Sonderzeichen anzeigen +general-settings.editor-show-whitespaces.title=Leerzeichen anzeigen +general-settings.tools-menu-tree-show-group-nodes.title=Tools im Menü in gruppieren +general-settings.tools-menu-tree-order-alphabetically.title=Tools im Menü alphabetisch sortieren +general-settings.show-internal-tools.title=Interne Werkzeuge anzeigen +general-settings.show-internal-tools.description=Aktiviert zusätzliche Entwicklerwerkzeuge, die nur für spezielle Anwendungen nützlich sind, z. B. für die Entwicklung von IntelliJ-Plugins. +general-settings.hide-workbench-tabs-on-single-tab.title=Arbeitsbereich-Tabs ausblenden, wenn nur ein Tab vorhanden ist +general-settings.dialog-is-modal.title=Dialog ist modal und muss geschlossen werden, bevor in IntelliJ weiterverwendet werden kann +general-settings.action-handling-instance.tool-window=Tool Window +general-settings.action-handling-instance.dialog=Dialog +general-settings.action-handling.auto-detect=Automatisch erkennen +general-settings.action-handling.selected=Ausgewählt: +json-handling-settings.title=JSON-Verarbeitung +json-handling-settings.write-group.title=Schreiben +json-handling-settings.read-group.title=Leasen +json-handling-settings.write-quote-field-names.title=Feldnamen in Anführungszeichen setzen +json-handling-settings.write-nan-as-strings.title=NaN als Zeichenkette +json-handling-settings.write-numbers-as-strings.title=Zahlen als Zeichenkette +json-handling-settings.write-escape-non-ascii.title=Nicht-ASCII-Zeichen escapen +json-handling-settings.write-hex-uppercase.title=Hexadezimal in Großbuchstaben +json-handling-settings.write-intention-spaces.title=Einrückung-Leerzeichen: +json-handling-settings.write-separators-spacing.none=Keine +json-handling-settings.write-separators-spacing.before=Davor +json-handling-settings.write-separators-spacing.after=Danach +json-handling-settings.write-separators-spacing.both=Beide +json-handling-settings.write-spacing-object-field-value.title=Objekt-Feld-Abstand: +json-handling-settings.write-spacing-object-entry.title=Objekt-Eintrag-Abstand: +json-handling-settings.write-spacing-array-value.title=Array-Wert-Abstand: +json-handling-settings.read-allow-java-comments.title=Java-Kommentare erlauben +json-handling-settings.read-allow-yaml-comments.title=YAML-Kommentare erlauben +json-handling-settings.read-allow-single-quotes.title=Einfache Anführungszeichen erlauben +json-handling-settings.read-allow-unquoted-field-names.title=Feldnamen ohne Anführungszeichen erlauben +json-handling-settings.read-allow-unescaped-control-characters.title=Unescaped Steuerzeichen erlauben +json-handling-settings.read-allow-backslash-escaping-any-character.title=Backslash-Escaping für alle Zeichen erlauben +json-handling-settings.read-allow-leading-zeros-for-numbers.title=Führende Nullen bei Zahlen erlauben +json-handling-settings.read-allow-leading-plus-sign-for-numbers.title=Führendes Pluszeichen bei Zahlen erlauben +json-handling-settings.read-allow-leading-decimal-point-for-numbers.title=Führenden Dezimalpunkt bei Zahlen erlauben +json-handling-settings.read-allow-trailing-decimal-point-for-numbers.title=Nachgestellten Dezimalpunkt bei Zahlen erlauben +json-handling-settings.read-allow-non-numeric-numbers.title=Nichtnumerische Zahlen erlauben +json-handling-settings.read-allow-missing-values.title=Fehlende Werte erlauben +json-handling-settings.read-allow-trailing-comma.title=Nachgestelltes Komma erlauben diff --git a/modules/settings/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsApplicationSettingsTest.kt b/modules/settings/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsApplicationSettingsTest.kt new file mode 100644 index 00000000..d09c572b --- /dev/null +++ b/modules/settings/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/DeveloperToolsApplicationSettingsTest.kt @@ -0,0 +1,227 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings + +import com.intellij.openapi.util.JDOMUtil +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.AnySettingProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.Settings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsHandler.settingsContainer +import org.assertj.core.api.Assertions.assertThat +import org.jdom.Element +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource + +class DeveloperToolsApplicationSettingsTest { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @ParameterizedTest(name = "{1}") + @MethodSource("settingsTestVectors") + fun `test that the settings names have not been changed`( + getSettings: (DeveloperToolsApplicationSettings) -> Settings, + @Suppress("unused") xmlElementName: String, + expectedSettingNames: Set, + ) { + val developerToolsApplicationSettings = DeveloperToolsApplicationSettings() + + assertThat( + getSettings(developerToolsApplicationSettings).settingsContainer().settingProperties.keys + ) + .containsExactlyInAnyOrderElementsOf(expectedSettingNames) + } + + @ParameterizedTest(name = "{1}") + @MethodSource("settingsTestVectors") + fun `test restoring from XML`( + getSettings: (DeveloperToolsApplicationSettings) -> Settings, + xmlElementName: String, + @Suppress("unused") expectedSettingNames: Set, + ) { + val developerToolsApplicationSettings = DeveloperToolsApplicationSettings() + val settingsContainer = getSettings(developerToolsApplicationSettings).settingsContainer() + + val modifiedAttributes = + settingsContainer.settingProperties + .map { (settingName, property) -> settingName to property.get().modifyValue() } + .toMap() + + val xmlAttributes = + modifiedAttributes + .map { (settingName, modifiedValue) -> + val persistentValue = + when (modifiedValue) { + is Boolean -> modifiedValue.toString() + is Int -> modifiedValue.toString() + is Enum<*> -> modifiedValue.name + else -> error("Unsupported type: ${this::class}") + } + "${settingName}=\"${persistentValue}\"" + } + .joinToString(" ") + val legacyState = + """ + + <$xmlElementName $xmlAttributes /> + + """ + .parseXml() + + developerToolsApplicationSettings.loadState(legacyState) + + settingsContainer.settingProperties.forEach { (settingName, property) -> + assertThat(property.isModified()).describedAs("$xmlElementName - $settingName").isTrue + + assertThat(property.get()) + .describedAs("$xmlElementName - $settingName") + .isEqualTo(modifiedAttributes[settingName]) + } + } + + @ParameterizedTest(name = "{1}") + @MethodSource("settingsTestVectors") + fun `test persisting to XML`( + getSettings: (DeveloperToolsApplicationSettings) -> Settings, + xmlElementName: String, + @Suppress("unused") expectedSettingNames: Set, + ) { + val developerToolsApplicationSettings = DeveloperToolsApplicationSettings() + val settingsContainer = getSettings(developerToolsApplicationSettings).settingsContainer() + + val expectedAttributes: Map = + settingsContainer.settingProperties + .map { (settingName, property) -> + val modifiedValue = property.get().modifyValue() + property.set(modifiedValue) + val expectedPersistentValue = + when (modifiedValue) { + is Boolean -> modifiedValue.toString() + is Int -> modifiedValue.toString() + is Enum<*> -> modifiedValue.name + else -> error("Unsupported type: ${this::class}") + } + settingName to expectedPersistentValue + } + .toMap() + + val actualState: Element = developerToolsApplicationSettings.state + val settingsElement: Element = + actualState.content.find { (it as Element).name == xmlElementName } as Element + assertThat(settingsElement).isNotNull + + assertThat(settingsElement.attributes.associate { it.name to it.value }) + .containsAllEntriesOf(expectedAttributes) + } + + @Test + fun `test loading legacy state`() { + val developerToolsApplicationSettings = DeveloperToolsApplicationSettings() + + val legacyAttributeNames = + listOf( + "addOpenMainDialogActionToMainToolbar", + "autoDetectActionHandlingInstance", + "editorShowSpecialCharacters", + "editorShowWhitespaces", + "editorSoftWraps", + "hideWorkbenchTabsOnSingleTab", + "loadExamples", + // Removed with 6.4.0 + // "promoteAddOpenMainDialogActionToMainToolbar", + "saveConfigurations", + "saveInputs", + "saveSensitiveInputs", + "selectedActionHandlingInstance", + "showInternalTools", + "toolsMenuTreeOrderAlphabetically", + "toolsMenuTreeShowGroupNodes", + ) + + val modifiedLegacyAttributes: Map = + legacyAttributeNames.associateWith { legacyAttributeName -> + val initialValue = + developerToolsApplicationSettings.generalSettings + .getSetting(legacyAttributeName) + .get() + initialValue.modifyValue() + } + + val legacyState = + """ + + """ + .parseXml() + developerToolsApplicationSettings.loadState(legacyState) + + legacyAttributeNames.forEach { legacyAttributeName -> + val settingProperty = + developerToolsApplicationSettings.generalSettings.getSetting< + Any, + Annotation, + AnySettingProperty, + >( + legacyAttributeName + ) + assertThat(settingProperty.isModified()).describedAs(legacyAttributeName).isTrue + assertThat(settingProperty.get()) + .describedAs(legacyAttributeName) + .isEqualTo(modifiedLegacyAttributes[legacyAttributeName]) + } + } + + private fun Any.modifyValue(): Any = + when (this) { + is Boolean -> !this + is Int -> this + 1 + is Enum<*> -> { + val enumConstants = this::class.java.enumConstants + enumConstants[(this.ordinal + 1) % enumConstants.size] + } + + else -> error("Unsupported type: ${this::class}") + } + + // -- Private Methods ----------------------------------------------------- // + + private fun String.parseXml(): Element = JDOMUtil.load(this) + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + @JvmStatic + fun settingsTestVectors() = + listOf( + arguments( + { it: DeveloperToolsApplicationSettings -> it.generalSettings }, + "GeneralSettings", + setOf( + "addOpenMainDialogActionToMainToolbar", + "loadExamples", + "saveConfigurations", + "saveInputs", + "saveSensitiveInputs", + "editorSoftWraps", + "editorShowSpecialCharacters", + "editorShowWhitespaces", + "toolsMenuTreeShowGroupNodes", + "toolsMenuTreeOrderAlphabetically", + "autoDetectActionHandlingInstance", + "selectedActionHandlingInstance", + "showInternalTools", + "hideWorkbenchTabsOnSingleTab", + "dialogIsModal", + ), + ), + arguments( + { it: DeveloperToolsApplicationSettings -> it.internalSettings }, + "InternalSettings", + setOf(), + ), + ) + } +} diff --git a/modules/settings/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/message/SettingsBundleMessagesTest.kt b/modules/settings/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/message/SettingsBundleMessagesTest.kt new file mode 100644 index 00000000..36baec9c --- /dev/null +++ b/modules/settings/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/settings/message/SettingsBundleMessagesTest.kt @@ -0,0 +1,83 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.settings.message + +import com.intellij.testFramework.junit5.RunMethodInEdt +import dev.turingcomplete.intellijdevelopertoolsplugin.common.nameWithoutExtension +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.BundleMessagesTest +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.Setting +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.base.SettingsGroup +import java.nio.file.Path +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtProperty +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.TestFactory + +class SettingsBundleMessagesTest : BundleMessagesTest() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @TestFactory + override fun `test that all additional languages are containing the same message keys and parameter counts`(): + List = + `do test that all additional languages are containing the same message keys and parameter counts`() + + @TestFactory + @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) + override fun `test that all message(messageKey, params) calls are referencing to an existing message key and are using the correct parameters count`(): + List = + `do test that all message(messageKey, params) calls are referencing to an existing message key and are using the correct parameters count`() + + @TestFactory + @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) + override fun `test that all keys in the messages bundle are used`(): List = + `do test that all keys in the messages bundle are used`() + + override fun kotlinSourceFilesToScanForMessagesBundleUsages(): List { + return super.kotlinSourceFilesToScanForMessagesBundleUsages().filter { + // Will falsely see `modificationsCounter` LDC instruction as `message` argument + it.fileName.toString() != "SettingsHandler\$SettingsContainer.class" + } + } + + override fun collectAllMessageBundleUsages(): List { + val allMessageBundleUsages = super.collectAllMessageBundleUsages().toMutableList() + + val settingsRelatedAnnotations = + setOf(Setting::class.simpleName!!, SettingsGroup::class.simpleName!!) + + kotlinSourceFilesToScanForMessagesBundleUsages().forEach { kotlinSourceFile -> + val toKtFile = kotlinSourceFile.toKtFile() + toKtFile + .traverseElement(KtProperty::class) + .plus(toKtFile.traverseElement(KtClassOrObject::class)) + .flatMap { it.annotationEntries } + .filter { settingsRelatedAnnotations.contains(it.shortName!!.asString()) } + .map { + val args = it.valueArguments.associateBy { it.getArgumentName()?.asName?.asString() } + + listOfNotNull( + "titleBundleKey" to args["titleBundleKey"], + "descriptionBundleKey" to args["descriptionBundleKey"], + ) + .map { it.first to it.second?.getArgumentExpression()?.text?.trim('"') } + .filter { (_, messageKey) -> messageKey != null } + .map { (type, messageKey) -> + MessagesBundleUsage( + className = kotlinSourceFile.nameWithoutExtension(), + bundleName = SettingsBundle::class.simpleName!!, + messageKey = messageKey!!, + parametersCount = 0, + displayableText = "$messageKey ($type)", + ) + } + .forEach { allMessageBundleUsages.add(it) } + } + } + + return allMessageBundleUsages + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/editor/build.gradle.kts b/modules/tools/editor/build.gradle.kts new file mode 100644 index 00000000..021aacbd --- /dev/null +++ b/modules/tools/editor/build.gradle.kts @@ -0,0 +1,20 @@ +dependencies { + implementation(project(":common")) + // This is required for the `OpenDeveloperToolService` mechanism. However, a + // better solution would be to have a common module that utilise the message + // bus to open ui tools from other modules. + implementation(project(":tools-ui")) + + implementation(libs.commons.text) + implementation(libs.bundles.text.case.converter) + implementation(libs.ulid.creator) + implementation(libs.jnanoid) + implementation(libs.uuid.generator) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + testImplementation(libs.assertj.core) + testImplementation(libs.bundles.junit.implementation) + testRuntimeOnly(libs.bundles.junit.runtime) + testImplementation(testFixtures(project(":common"))) +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/DataGenerators.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/DataGenerators.kt similarity index 51% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/DataGenerators.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/DataGenerators.kt index 9e128003..7491d64d 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/DataGenerators.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/DataGenerators.kt @@ -1,40 +1,41 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor import com.aventrix.jnanoid.jnanoid.NanoIdUtils import com.fasterxml.uuid.Generators import com.github.f4b6a3.ulid.UlidCreator import dev.turingcomplete.intellijdevelopertoolsplugin.common.HashingUtils -import org.jetbrains.annotations.Nls +import dev.turingcomplete.intellijdevelopertoolsplugin.common.toHexString import java.time.Instant import java.time.ZoneId import java.time.ZoneOffset import java.time.format.DateTimeFormatter -import java.util.* +import java.util.Locale +import org.jetbrains.annotations.Nls -internal object DataGenerators { - // -- Variables --------------------------------------------------------------------------------------------------- // +object DataGenerators { + // -- Variables ----------------------------------------------------------- // private val uuidV7Generator = UuidV7Generator() - val dataGenerators: List = listOf( - UuidV4Generator(), - uuidV7Generator, - UlidGenerator(), - NanoIdGenerator(), - DataGeneratorsGroup("Current Date and Time", createCurrentDateAndTimeGenerators()), - DataGeneratorsGroup("Current Unix Timestamp", createUnixTimestampGenerators()), - DataGeneratorsGroup("Random Hash", createRandomHashGenerators()) - ) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - + val dataGenerators: List = + listOf( + UuidV4Generator(), + uuidV7Generator, + UlidGenerator(), + NanoIdGenerator(), + DataGeneratorsGroup("Current Date and Time", createCurrentDateAndTimeGenerators()), + DataGeneratorsGroup("Current Unix Timestamp", createUnixTimestampGenerators()), + DataGeneratorsGroup("Random Hash", createRandomHashGenerators()), + ) + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun createRandomHashGenerators(): List = HashingUtils.commonMessageDigests.map { messageDigest -> - object : DataGenerator(messageDigest.algorithm, "Generate random ${messageDigest.algorithm}") { + object : + DataGenerator(messageDigest.algorithm, "Generate random ${messageDigest.algorithm}") { - @OptIn(ExperimentalStdlibApi::class) override fun generate(): String = messageDigest.digest(uuidV7Generator.generate().encodeToByteArray()).toHexString() } @@ -42,65 +43,71 @@ internal object DataGenerators { private fun createUnixTimestampGenerators(): List = linkedMapOf( - "Seconds" to { System.currentTimeMillis().div(1000).toString() }, - "Milliseconds" to { System.currentTimeMillis().toString() }, - "Nanoseconds" to { System.nanoTime().toString() }, - ).map { (name, generateUnixTimestamp) -> - object : DataGenerator(name, "Insert current UNIX timestamp (${name.lowercase()})") { - - override fun generate(): String = generateUnixTimestamp() + "Seconds" to { System.currentTimeMillis().div(1000).toString() }, + "Milliseconds" to { System.currentTimeMillis().toString() }, + "Nanoseconds" to { System.nanoTime().toString() }, + ) + .map { (name, generateUnixTimestamp) -> + object : DataGenerator(name, "Insert current UNIX timestamp (${name.lowercase()})") { + + override fun generate(): String = generateUnixTimestamp() + } } - } private fun createCurrentDateAndTimeGenerators(): List = listOf( - Triple("ISO-8601 date time with time zone", "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", ZoneId.systemDefault()), - Triple("ISO-8601 date time at UTC", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", ZoneOffset.UTC), - Triple("ISO-8601 date", "yyyy-MM-dd", ZoneId.systemDefault()), - Triple("ISO-8601 time", "HH:mm:ss", ZoneId.systemDefault()), - Triple("ISO-8601 ordinal date", "yyyy-DDD", ZoneId.systemDefault()), - Triple("ISO-8601 week date", "YYYY-'W'ww-e", ZoneId.systemDefault()), - Triple("RFC-1123 date time", "EEE, dd MMM yyyy HH:mm:ss", ZoneOffset.UTC) - ).map { (name, pattern, timeZone) -> - object : DataGenerator(pattern, name,"Insert current date time using format: $pattern") { - - override fun generate(): String = - DateTimeFormatter.ofPattern(pattern) - .withLocale(Locale.getDefault()) - .withZone(timeZone) - .format(Instant.now()) + Triple( + "ISO-8601 date time with time zone", + "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", + ZoneId.systemDefault(), + ), + Triple("ISO-8601 date time at UTC", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", ZoneOffset.UTC), + Triple("ISO-8601 date", "yyyy-MM-dd", ZoneId.systemDefault()), + Triple("ISO-8601 time", "HH:mm:ss", ZoneId.systemDefault()), + Triple("ISO-8601 ordinal date", "yyyy-DDD", ZoneId.systemDefault()), + Triple("ISO-8601 week date", "YYYY-'W'ww-e", ZoneId.systemDefault()), + Triple("RFC-1123 date time", "EEE, dd MMM yyyy HH:mm:ss", ZoneOffset.UTC), + ) + .map { (name, pattern, timeZone) -> + object : DataGenerator(pattern, name, "Insert current date time using format: $pattern") { + + override fun generate(): String = + DateTimeFormatter.ofPattern(pattern) + .withLocale(Locale.getDefault()) + .withZone(timeZone) + .format(Instant.now()) + } } - } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class UuidV4Generator : DataGenerator("UUIDv4") { override fun generate(): String = Generators.randomBasedGenerator().generate().toString() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class UuidV7Generator : DataGenerator("UUIDv7") { override fun generate(): String = Generators.timeBasedEpochGenerator().generate().toString() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class UlidGenerator : DataGenerator("ULID") { override fun generate(): String = UlidCreator.getUlid().toString() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class NanoIdGenerator : DataGenerator("Nano ID") { override fun generate(): String = NanoIdUtils.randomNanoId() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // sealed interface DataGeneratorBase { @@ -108,22 +115,22 @@ internal object DataGenerators { val toolText: String? } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // abstract class DataGenerator( @Nls(capitalization = Nls.Capitalization.Title) override val title: String, val actionName: String = "Insert generated $title", - @Nls(capitalization = Nls.Capitalization.Sentence) override val toolText: String? = null + @Nls(capitalization = Nls.Capitalization.Sentence) override val toolText: String? = null, ) : DataGeneratorBase { abstract fun generate(): String } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class DataGeneratorsGroup( @Nls(capitalization = Nls.Capitalization.Title) override val title: String, val children: List, - @Nls(capitalization = Nls.Capitalization.Sentence) override val toolText: String? = null + @Nls(capitalization = Nls.Capitalization.Sentence) override val toolText: String? = null, ) : DataGeneratorBase -} \ No newline at end of file +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/EncodersDecoders.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/EncodersDecoders.kt similarity index 52% rename from modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/EncodersDecoders.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/EncodersDecoders.kt index aebee8d7..a21ff677 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/EncodersDecoders.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/EncodersDecoders.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.common.tool +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.diagnostic.logger @@ -10,65 +10,75 @@ import dev.turingcomplete.intellijdevelopertoolsplugin.common.HashingUtils import dev.turingcomplete.intellijdevelopertoolsplugin.common.decodeFromAscii import dev.turingcomplete.intellijdevelopertoolsplugin.common.encodeToAscii import dev.turingcomplete.intellijdevelopertoolsplugin.common.toHexString -import org.apache.commons.codec.binary.Base32 import java.net.URLDecoder import java.net.URLEncoder import java.nio.charset.StandardCharsets -import java.util.* +import java.util.Base64 +import org.apache.commons.codec.binary.Base32 -internal object EncodersDecoders { - // -- Variables --------------------------------------------------------------------------------------------------- // +object EncodersDecoders { + // -- Variables ----------------------------------------------------------- // private val log = logger() val commonEncoders: List - val commonDecoders = listOf( - Decoder("Base32", { Base32().decode(it).decodeToString() }), - Decoder("Base64", { Base64.getDecoder().decode(it).decodeToString() }), - Decoder("MIME Base64", { Base64.getMimeDecoder().decode(it).decodeToString() }), - Decoder("URL Base64", { Base64.getUrlDecoder().decode(it).decodeToString() }), - Decoder("URL", { URLDecoder.decode(it, StandardCharsets.UTF_8) }), - Decoder("ASCII", { it.decodeFromAscii() }) - ) + val commonDecoders = + listOf( + Decoder("Base32", { Base32().decode(it).decodeToString() }), + Decoder("Base64", { Base64.getDecoder().decode(it).decodeToString() }), + Decoder("MIME Base64", { Base64.getMimeDecoder().decode(it).decodeToString() }), + Decoder("URL Base64", { Base64.getUrlDecoder().decode(it).decodeToString() }), + Decoder("URL", { URLDecoder.decode(it, StandardCharsets.UTF_8) }), + Decoder("ASCII", { it.decodeFromAscii() }), + ) - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { - val commonEncoders = mutableListOf( - Encoder("Base32", { Base32().encodeToString(it.encodeToByteArray()) }), - Encoder("Base64", { Base64.getEncoder().encodeToString(it.encodeToByteArray()) }), - Encoder("MIME Base64", { Base64.getMimeEncoder().encodeToString(it.encodeToByteArray()) }), - Encoder("URL Base64", { Base64.getUrlEncoder().encodeToString(it.encodeToByteArray()) }), - Encoder("URL", { URLEncoder.encode(it, StandardCharsets.UTF_8) }), - Encoder("ASCII", { it.encodeToAscii() }), - ) + val commonEncoders = + mutableListOf( + Encoder("Base32", { Base32().encodeToString(it.encodeToByteArray()) }), + Encoder("Base64", { Base64.getEncoder().encodeToString(it.encodeToByteArray()) }), + Encoder("MIME Base64", { Base64.getMimeEncoder().encodeToString(it.encodeToByteArray()) }), + Encoder("URL Base64", { Base64.getUrlEncoder().encodeToString(it.encodeToByteArray()) }), + Encoder("URL", { URLEncoder.encode(it, StandardCharsets.UTF_8) }), + Encoder("ASCII", { it.encodeToAscii() }), + ) HashingUtils.commonMessageDigests.forEach { messageDigest -> - commonEncoders.add(Encoder(messageDigest.algorithm, { messageDigest.digest(it.encodeToByteArray()).toHexString() })) + commonEncoders.add( + Encoder( + messageDigest.algorithm, + { messageDigest.digest(it.encodeToByteArray()).toHexString() }, + ) + ) } this.commonEncoders = commonEncoders } - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // fun executeEncodingInEditor( text: String, textRange: TextRange, encoder: Encoder, - editor: Editor + editor: Editor, ) { try { val result = encoder.encode(text) editor.executeWriteCommand(encoder.actionName) { it.document.replaceString(textRange.startOffset, textRange.endOffset, result) } - } - catch (e: Exception) { + } catch (e: Exception) { log.warn("Encoding failed", e) ApplicationManager.getApplication().invokeLater { - Messages.showErrorDialog(editor.project, "Encoding failed: ${e.message}", encoder.actionName) + Messages.showErrorDialog( + editor.project, + "Encoding failed: ${e.message}", + encoder.actionName, + ) } } } @@ -77,28 +87,39 @@ internal object EncodersDecoders { text: String, textRange: TextRange, decoder: Decoder, - editor: Editor + editor: Editor, ) { try { val result = decoder.decode(text) editor.executeWriteCommand(decoder.actionName) { it.document.replaceString(textRange.startOffset, textRange.endOffset, result) } - } - catch (e: Exception) { + } catch (e: Exception) { log.warn("Decoding failed", e) ApplicationManager.getApplication().invokeLater { - Messages.showErrorDialog(editor.project, "Decoding failed: ${e.message}", decoder.actionName) + Messages.showErrorDialog( + editor.project, + "Decoding failed: ${e.message}", + decoder.actionName, + ) } } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - class Encoder(val title: String, val encode: (String) -> String, val actionName: String = "Encode to $title") + class Encoder( + val title: String, + val encode: (String) -> String, + val actionName: String = "Encode to $title", + ) - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - class Decoder(val title: String, val decode: (String) -> String, val actionName: String = "Decode from $title") -} \ No newline at end of file + class Decoder( + val title: String, + val decode: (String) -> String, + val actionName: String = "Decode from $title", + ) +} diff --git a/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/EscapersUnescapers.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/EscapersUnescapers.kt new file mode 100644 index 00000000..2eb64e83 --- /dev/null +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/EscapersUnescapers.kt @@ -0,0 +1,90 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.TextRange +import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.executeWriteCommand +import org.apache.commons.text.StringEscapeUtils + +object EscapersUnescapers { + // -- Variables ----------------------------------------------------------- // + + private val log = logger() + + val commonEscaper = + listOf( + Escaper("Java String", { StringEscapeUtils.escapeJava(it) }), + Escaper("HTML Entities", { StringEscapeUtils.escapeHtml4(it) }), + Escaper("JSON Value", { StringEscapeUtils.escapeJson(it) }), + Escaper("XML Value", { StringEscapeUtils.escapeXml11(it) }), + Escaper("CSV Value", { StringEscapeUtils.escapeCsv(it) }), + ) + + val commonUnescaper = + listOf( + Unescaper("Java String", { StringEscapeUtils.unescapeJava(it) }), + Unescaper("HTML Entities", { StringEscapeUtils.escapeHtml4(it) }), + Unescaper("JSON Value", { StringEscapeUtils.unescapeJson(it) }), + Unescaper("XML Value", { StringEscapeUtils.unescapeCsv(it) }), + Unescaper("CSV Value", { StringEscapeUtils.unescapeCsv(it) }), + ) + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun executeEscapeInEditor(text: String, textRange: TextRange, escaper: Escaper, editor: Editor) { + try { + val result = escaper.escape(text) + editor.executeWriteCommand(escaper.actionName) { + it.document.replaceString(textRange.startOffset, textRange.endOffset, result) + } + } catch (e: Exception) { + log.warn("Escape failed", e) + ApplicationManager.getApplication().invokeLater { + Messages.showErrorDialog(editor.project, "Escape failed: ${e.message}", escaper.actionName) + } + } + } + + fun executeUnescapeInEditor( + text: String, + textRange: TextRange, + unescaper: Unescaper, + editor: Editor, + ) { + try { + val result = unescaper.unescape(text) + editor.executeWriteCommand(unescaper.actionName) { + it.document.replaceString(textRange.startOffset, textRange.endOffset, result) + } + } catch (e: Exception) { + log.warn("Unescape failed", e) + ApplicationManager.getApplication().invokeLater { + Messages.showErrorDialog( + editor.project, + "Unescape failed: ${e.message}", + unescaper.actionName, + ) + } + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + class Escaper( + val title: String, + val escape: (String) -> String, + val actionName: String = "Escape $title", + ) + + // -- Inner Type ---------------------------------------------------------- // + + class Unescaper( + val title: String, + val unescape: (String) -> String, + val actionName: String = "Unescape $title", + ) +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/DataGeneratorActionGroup.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/DataGeneratorActionGroup.kt similarity index 54% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/DataGeneratorActionGroup.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/DataGeneratorActionGroup.kt index 6ba022d8..4eec4adc 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/DataGeneratorActionGroup.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/DataGeneratorActionGroup.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.action +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -6,19 +6,20 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.project.DumbAwareAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.DataGenerators.DataGenerator -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.DataGenerators.DataGeneratorBase -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.DataGenerators.DataGeneratorsGroup -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.DataGenerators.dataGenerators import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.executeWriteCommand +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.DataGenerators.DataGenerator +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.DataGenerators.DataGeneratorBase +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.DataGenerators.DataGeneratorsGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.DataGenerators.dataGenerators -internal class DataGeneratorActionGroup : DefaultActionGroup("Insert Generated Data", true) { - // -- Properties -------------------------------------------------------------------------------------------------- // +class DataGeneratorActionGroup : DefaultActionGroup("Insert Generated Data", true) { + // -- Properties ---------------------------------------------------------- // - private val dataGeneratorActions: Array = dataGenerators.map { createDataGeneratorAction(it) }.toTypedArray() + private val dataGeneratorActions: Array = + dataGenerators.map { createDataGeneratorAction(it) }.toTypedArray() - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun getChildren(e: AnActionEvent?): Array = dataGeneratorActions @@ -29,22 +30,23 @@ internal class DataGeneratorActionGroup : DefaultActionGroup("Insert Generated D override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // - private fun createDataGeneratorAction(dataGeneratorBase: DataGeneratorBase): AnAction = when(dataGeneratorBase) { - is DataGenerator -> DataGeneratorAction(dataGeneratorBase) - is DataGeneratorsGroup -> object: DefaultActionGroup(dataGeneratorBase.title, true) { + private fun createDataGeneratorAction(dataGeneratorBase: DataGeneratorBase): AnAction = + when (dataGeneratorBase) { + is DataGenerator -> DataGeneratorAction(dataGeneratorBase) + is DataGeneratorsGroup -> + object : DefaultActionGroup(dataGeneratorBase.title, true) { - override fun getChildren(e: AnActionEvent?): Array = - dataGeneratorBase.children.map { createDataGeneratorAction(it) }.toTypedArray() + override fun getChildren(e: AnActionEvent?): Array = + dataGeneratorBase.children.map { createDataGeneratorAction(it) }.toTypedArray() + } } - } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class DataGeneratorAction( - private val dataGenerator: DataGenerator - ) : DumbAwareAction(dataGenerator.title, dataGenerator.toolText, null) { + private class DataGeneratorAction(private val dataGenerator: DataGenerator) : + DumbAwareAction(dataGenerator.title, dataGenerator.toolText, null) { override fun actionPerformed(e: AnActionEvent) { val editor = e.getData(CommonDataKeys.EDITOR) ?: return @@ -54,8 +56,7 @@ internal class DataGeneratorActionGroup : DefaultActionGroup("Insert Generated D val selectionEnd = editor.selectionModel.selectionEnd if (selectionEnd > selectionStart) { it.document.replaceString(selectionStart, selectionEnd, result) - } - else { + } else { val currentOffset = editor.caretModel.offset it.document.insertString(currentOffset, result) editor.caretModel.moveToOffset(currentOffset + result.length) @@ -64,5 +65,5 @@ internal class DataGeneratorActionGroup : DefaultActionGroup("Insert Generated D } } - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/DeveloperToolsActionGroup.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/DeveloperToolsActionGroup.kt new file mode 100644 index 00000000..5e8b744f --- /dev/null +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/DeveloperToolsActionGroup.kt @@ -0,0 +1,29 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action + +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.ui.IconManager + +class DeveloperToolsActionGroup : DefaultActionGroup("Developer Tools", true) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + + init { + templatePresentation.icon = icon + templatePresentation.isHideGroupIfEmpty = true + } + + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val icon = + IconManager.getInstance() + .getIcon( + "dev/turingcomplete/intellijdevelopertoolsplugin/icons/action.svg", + DeveloperToolsActionGroup::class.java.classLoader, + ) + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/EditorTextStatisticAction.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EditorTextStatisticAction.kt similarity index 51% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/EditorTextStatisticAction.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EditorTextStatisticAction.kt index 59e4cf04..b4cb0013 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/EditorTextStatisticAction.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EditorTextStatisticAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.action +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent @@ -6,34 +6,35 @@ import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.application.runReadAction import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAwareAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other.TextStatistic -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other.TextStatistic.Companion -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolService import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.getSelectedText +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other.TextStatistic -internal class EditorTextStatisticAction : DumbAwareAction("Show Text Statistic of Document...") { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class EditorTextStatisticAction : DumbAwareAction("Show Text Statistic of Document...") { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun update(e: AnActionEvent) { - e.presentation.isEnabledAndVisible = e.getData(CommonDataKeys.PROJECT) != null - && e.getData(CommonDataKeys.EDITOR) != null + e.presentation.isEnabledAndVisible = + e.getData(CommonDataKeys.PROJECT) != null && e.getData(CommonDataKeys.EDITOR) != null } override fun actionPerformed(e: AnActionEvent) { val project = e.getData(CommonDataKeys.PROJECT) ?: return val editor = e.getData(CommonDataKeys.EDITOR) ?: return val text = runReadAction { editor.getSelectedText()?.first ?: editor.document.text } - project.service().openTool( - TextStatistic.OpenTextStatisticContext(text), - Companion.openTextStatisticReference - ) + project + .service() + .openTool( + TextStatistic.OpenTextStatisticContext(text), + TextStatistic.openTextStatisticReference, + ) } override fun getActionUpdateThread() = ActionUpdateThread.BGT - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/EncodeDecodeActionGroup.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EncodeDecodeActionGroup.kt similarity index 63% rename from modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/EncodeDecodeActionGroup.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EncodeDecodeActionGroup.kt index 08558a24..db8a2cd2 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/EncodeDecodeActionGroup.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EncodeDecodeActionGroup.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -8,43 +8,43 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.util.TextRange import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.getSelectedText -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.Encoder -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.executeDecodingInEditor -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.executeEncodingInEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.Encoder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.executeDecodingInEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.executeEncodingInEditor open class EncodeDecodeActionGroup : DefaultActionGroup("Encoder/Decoder", false) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val encoderActionGroup by lazy { createActionGroup( title = "Encode To", - actions = EncodersDecoders.commonEncoders.map { encoder -> - EncoderAction(encoder) { getSourceText(it) } - } + actions = + EncodersDecoders.commonEncoders.map { encoder -> + EncoderAction(encoder) { getSourceText(it) } + }, ) } private val decoderActionGroup by lazy { createActionGroup( title = "Decode From", - actions = EncodersDecoders.commonDecoders.map { decoder -> - DecoderAction(decoder) { getSourceText(it) } - } + actions = + EncodersDecoders.commonDecoders.map { decoder -> + DecoderAction(decoder) { getSourceText(it) } + }, ) } private val encoderDecoderActions: Array by lazy { - arrayOf( - encoderActionGroup, - decoderActionGroup - ) + arrayOf(encoderActionGroup, decoderActionGroup) } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // final override fun update(e: AnActionEvent) { val editor = e.getData(EDITOR) - e.presentation.isVisible = editor != null && editor.document.isWritable && getSourceText(e) != null + e.presentation.isVisible = + editor != null && editor.document.isWritable && getSourceText(e) != null } final override fun getChildren(e: AnActionEvent?): Array = encoderDecoderActions @@ -56,20 +56,21 @@ open class EncodeDecodeActionGroup : DefaultActionGroup("Encoder/Decoder", false return editor.getSelectedText() } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // - private fun createActionGroup(title: String, actions: List) = object : DefaultActionGroup(title, true) { + private fun createActionGroup(title: String, actions: List) = + object : DefaultActionGroup(title, true) { - private val decoderActions: Array = actions.toTypedArray() + private val decoderActions: Array = actions.toTypedArray() - override fun getChildren(e: AnActionEvent?): Array = decoderActions - } + override fun getChildren(e: AnActionEvent?): Array = decoderActions + } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class EncoderAction( val encoder: Encoder, - val getSourceText: (AnActionEvent) -> Pair? + val getSourceText: (AnActionEvent) -> Pair?, ) : DumbAwareAction(encoder.title, encoder.actionName, null) { override fun actionPerformed(e: AnActionEvent) { @@ -79,11 +80,11 @@ open class EncodeDecodeActionGroup : DefaultActionGroup("Encoder/Decoder", false } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class DecoderAction( val decoder: EncodersDecoders.Decoder, - val getSourceText: (AnActionEvent) -> Pair? + val getSourceText: (AnActionEvent) -> Pair?, ) : DumbAwareAction(decoder.title, decoder.actionName, null) { override fun actionPerformed(e: AnActionEvent) { @@ -93,5 +94,5 @@ open class EncodeDecodeActionGroup : DefaultActionGroup("Encoder/Decoder", false } } - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/EscapeUnescapeActionGroup.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EscapeUnescapeActionGroup.kt similarity index 61% rename from modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/EscapeUnescapeActionGroup.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EscapeUnescapeActionGroup.kt index 0818cd26..ae0bafc7 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/EscapeUnescapeActionGroup.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/EscapeUnescapeActionGroup.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -8,44 +8,44 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.util.TextRange import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.getSelectedText -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.Escaper -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.Unescaper -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.executeEscapeInEditor -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.executeUnescapeInEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.Escaper +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.Unescaper +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.executeEscapeInEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.executeUnescapeInEditor open class EscapeUnescapeActionGroup : DefaultActionGroup("Escape/Unescape", false) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val escapeActionGroup by lazy { createActionGroup( title = "Escape", - actions = EscapersUnescapers.commonEscaper.map { escaper -> - EscapeAction(escaper) { getSourceText(it) } - } + actions = + EscapersUnescapers.commonEscaper.map { escaper -> + EscapeAction(escaper) { getSourceText(it) } + }, ) } private val unescapeActionGroup by lazy { createActionGroup( title = "Unescape", - actions = EscapersUnescapers.commonUnescaper.map { unescaper -> - UnescapeAction(unescaper) { getSourceText(it) } - } + actions = + EscapersUnescapers.commonUnescaper.map { unescaper -> + UnescapeAction(unescaper) { getSourceText(it) } + }, ) } private val encoderDecoderActions: Array by lazy { - arrayOf( - escapeActionGroup, - unescapeActionGroup - ) + arrayOf(escapeActionGroup, unescapeActionGroup) } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // final override fun update(e: AnActionEvent) { val editor = e.getData(EDITOR) - e.presentation.isVisible = editor != null && editor.document.isWritable && getSourceText(e) != null + e.presentation.isVisible = + editor != null && editor.document.isWritable && getSourceText(e) != null } final override fun getChildren(e: AnActionEvent?): Array = encoderDecoderActions @@ -57,20 +57,21 @@ open class EscapeUnescapeActionGroup : DefaultActionGroup("Escape/Unescape", fal return editor.getSelectedText() } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // - private fun createActionGroup(title: String, actions: List) = object : DefaultActionGroup(title, true) { + private fun createActionGroup(title: String, actions: List) = + object : DefaultActionGroup(title, true) { - private val decoderActions: Array = actions.toTypedArray() + private val decoderActions: Array = actions.toTypedArray() - override fun getChildren(e: AnActionEvent?): Array = decoderActions - } + override fun getChildren(e: AnActionEvent?): Array = decoderActions + } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class EscapeAction( val escaper: Escaper, - val getSourceText: (AnActionEvent) -> Pair? + val getSourceText: (AnActionEvent) -> Pair?, ) : DumbAwareAction(escaper.title, escaper.actionName, null) { override fun actionPerformed(e: AnActionEvent) { @@ -80,11 +81,11 @@ open class EscapeUnescapeActionGroup : DefaultActionGroup("Escape/Unescape", fal } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class UnescapeAction( val unescaper: Unescaper, - val getSourceText: (AnActionEvent) -> Pair? + val getSourceText: (AnActionEvent) -> Pair?, ) : DumbAwareAction(unescaper.title, unescaper.actionName, null) { override fun actionPerformed(e: AnActionEvent) { @@ -94,5 +95,5 @@ open class EscapeUnescapeActionGroup : DefaultActionGroup("Escape/Unescape", fal } } - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/TextCaseConverterActionGroup.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/TextCaseConverterActionGroup.kt similarity index 83% rename from modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/TextCaseConverterActionGroup.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/TextCaseConverterActionGroup.kt index 9a01621e..f27898a9 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/action/TextCaseConverterActionGroup.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/action/TextCaseConverterActionGroup.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.action +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.action import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -15,17 +15,18 @@ import dev.turingcomplete.intellijdevelopertoolsplugin.common.TextCaseUtils.dete import dev.turingcomplete.textcaseconverter.TextCase open class TextCaseConverterActionGroup : DefaultActionGroup("Convert Text Case To", true) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val textCasesAction: Array = allTextCases.map { ConvertTextCaseAction(it) { getSourceText(it) } }.toTypedArray() - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // final override fun update(e: AnActionEvent) { val editor = e.getData(CommonDataKeys.EDITOR) - e.presentation.isVisible = editor != null && editor.document.isWritable && getSourceText(e) != null + e.presentation.isVisible = + editor != null && editor.document.isWritable && getSourceText(e) != null } final override fun getChildren(e: AnActionEvent?): Array = textCasesAction @@ -37,12 +38,12 @@ open class TextCaseConverterActionGroup : DefaultActionGroup("Convert Text Case return editor.getSelectedText() } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class ConvertTextCaseAction( val textCase: TextCase, - val getSourceText: (AnActionEvent) -> Pair? + val getSourceText: (AnActionEvent) -> Pair?, ) : DumbAwareAction(textCase.example(), null, null) { override fun actionPerformed(e: AnActionEvent) { @@ -55,7 +56,7 @@ open class TextCaseConverterActionGroup : DefaultActionGroup("Convert Text Case text: String, textRange: TextRange, textCase: TextCase, - editor: Editor + editor: Editor, ) { val wordsSplitter = determineWordsSplitter(text, textCase) val result = textCase.convert(text, wordsSplitter) @@ -65,5 +66,5 @@ open class TextCaseConverterActionGroup : DefaultActionGroup("Convert Text Case } } - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/intention/DataGeneratorIntentionAction.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/DataGeneratorIntentionAction.kt similarity index 69% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/intention/DataGeneratorIntentionAction.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/DataGeneratorIntentionAction.kt index 0a027cf8..e5075e60 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/intention/DataGeneratorIntentionAction.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/DataGeneratorIntentionAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.intention +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.LowPriorityAction @@ -9,16 +9,16 @@ import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.openapi.ui.popup.PopupStep import com.intellij.openapi.ui.popup.util.BaseListPopupStep import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.DataGenerators.DataGenerator -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.DataGenerators.DataGeneratorBase -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.DataGenerators.DataGeneratorsGroup -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.DataGenerators.dataGenerators import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.executeWriteCommand +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.DataGenerators.DataGenerator +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.DataGenerators.DataGeneratorBase +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.DataGenerators.DataGeneratorsGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.DataGenerators.dataGenerators -internal class DataGeneratorIntentionAction : IntentionAction, LowPriorityAction { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class DataGeneratorIntentionAction : IntentionAction, LowPriorityAction { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun startInWriteAction(): Boolean = false @@ -27,7 +27,7 @@ internal class DataGeneratorIntentionAction : IntentionAction, LowPriorityAction override fun getText(): String = familyName override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = - editor?.document?.isWritable ?: false + editor?.document?.isWritable == true override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { if (editor == null) { @@ -41,19 +41,21 @@ internal class DataGeneratorIntentionAction : IntentionAction, LowPriorityAction } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class GenerateTextListPopupStep( dataGenerators: List, - private val editor: Editor + private val editor: Editor, ) : BaseListPopupStep(null, dataGenerators) { override fun getTextFor(dataGenerator: DataGeneratorBase): String = dataGenerator.title - override fun isFinal(dataGenerator: DataGeneratorBase?): Boolean = dataGenerator is DataGenerator + override fun isFinal(dataGenerator: DataGeneratorBase?): Boolean = + dataGenerator is DataGenerator - override fun hasSubstep(dataGenerator: DataGeneratorBase?): Boolean = dataGenerator is DataGeneratorsGroup + override fun hasSubstep(dataGenerator: DataGeneratorBase?): Boolean = + dataGenerator is DataGeneratorsGroup override fun onChosen(dataGenerator: DataGeneratorBase, finalChoice: Boolean): PopupStep<*>? { when (dataGenerator) { @@ -75,6 +77,6 @@ internal class DataGeneratorIntentionAction : IntentionAction, LowPriorityAction } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/EncodeDecodeIntentionAction.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/EncodeDecodeIntentionAction.kt similarity index 64% rename from modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/EncodeDecodeIntentionAction.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/EncodeDecodeIntentionAction.kt index a744d5a2..066d33aa 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/EncodeDecodeIntentionAction.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/EncodeDecodeIntentionAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.LowPriorityAction @@ -10,24 +10,25 @@ import com.intellij.openapi.ui.popup.PopupStep import com.intellij.openapi.ui.popup.util.BaseListPopupStep import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.Decoder -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.Encoder -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.commonDecoders -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.commonEncoders -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.executeDecodingInEditor -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EncodersDecoders.executeEncodingInEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.Decoder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.Encoder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.commonDecoders +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.commonEncoders +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.executeDecodingInEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EncodersDecoders.executeEncodingInEditor abstract class EncodeDecodeIntentionAction : IntentionAction, LowPriorityAction { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // final override fun startInWriteAction(): Boolean = false final override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = - editor != null && file != null - && editor.document.isWritable - && getSourceText(editor, file) != null + editor != null && + file != null && + editor.document.isWritable && + getSourceText(editor, file) != null final override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { if (editor == null || file == null) { @@ -45,40 +46,44 @@ abstract class EncodeDecodeIntentionAction : IntentionAction, LowPriorityAction abstract fun getSourceText(editor: Editor, file: PsiFile): Pair? - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class EncodersDecodersModeSelectionListPopupStep( text: String, textRange: TextRange, - editor: Editor - ) : BaseListPopupStep>( - null, - EncoderListPopupStep(text, textRange, editor), - DecoderListPopupStep(text, textRange, editor) - ) { + editor: Editor, + ) : + BaseListPopupStep>( + null, + EncoderListPopupStep(text, textRange, editor), + DecoderListPopupStep(text, textRange, editor), + ) { override fun hasSubstep(baseListPopupStep: EncoderDecoderListPopupStep<*>): Boolean = true - override fun getTextFor(baseListPopupStep: EncoderDecoderListPopupStep<*>): String = baseListPopupStep.actionName + override fun getTextFor(baseListPopupStep: EncoderDecoderListPopupStep<*>): String = + baseListPopupStep.actionName - override fun onChosen(baseListPopupStep: EncoderDecoderListPopupStep<*>, finalChoice: Boolean): PopupStep<*> = - baseListPopupStep + override fun onChosen( + baseListPopupStep: EncoderDecoderListPopupStep<*>, + finalChoice: Boolean, + ): PopupStep<*> = baseListPopupStep } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // interface EncoderDecoderListPopupStep : PopupStep { val actionName: String } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class EncoderListPopupStep( private val text: String, private val textRange: TextRange, - private val editor: Editor + private val editor: Editor, ) : BaseListPopupStep(null, commonEncoders), EncoderDecoderListPopupStep { override val actionName: String = "Encode To" @@ -91,12 +96,12 @@ abstract class EncodeDecodeIntentionAction : IntentionAction, LowPriorityAction } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class DecoderListPopupStep( private val text: String, private val textRange: TextRange, - private val editor: Editor + private val editor: Editor, ) : BaseListPopupStep(null, commonDecoders), EncoderDecoderListPopupStep { override val actionName: String = "Decode From" @@ -109,6 +114,6 @@ abstract class EncodeDecodeIntentionAction : IntentionAction, LowPriorityAction } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/EscapeUnescapeIntentionAction.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/EscapeUnescapeIntentionAction.kt similarity index 62% rename from modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/EscapeUnescapeIntentionAction.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/EscapeUnescapeIntentionAction.kt index 804a0700..0442312b 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/EscapeUnescapeIntentionAction.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/EscapeUnescapeIntentionAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.LowPriorityAction @@ -10,24 +10,25 @@ import com.intellij.openapi.ui.popup.PopupStep import com.intellij.openapi.ui.popup.util.BaseListPopupStep import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiFile -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.Escaper -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.Unescaper -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.commonEscaper -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.commonUnescaper -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.executeEscapeInEditor -import dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.EscapersUnescapers.executeUnescapeInEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.Escaper +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.Unescaper +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.commonEscaper +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.commonUnescaper +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.executeEscapeInEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.EscapersUnescapers.executeUnescapeInEditor abstract class EscapeUnescapeIntentionAction : IntentionAction, LowPriorityAction { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // final override fun startInWriteAction(): Boolean = false final override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = - editor != null && file != null - && editor.document.isWritable - && getSourceText(editor, file) != null + editor != null && + file != null && + editor.document.isWritable && + getSourceText(editor, file) != null final override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { if (editor == null || file == null) { @@ -45,40 +46,44 @@ abstract class EscapeUnescapeIntentionAction : IntentionAction, LowPriorityActio abstract fun getSourceText(editor: Editor, file: PsiFile): Pair? - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class EscapersUnescapersModeSelectionListPopupStep( text: String, textRange: TextRange, - editor: Editor - ) : BaseListPopupStep>( - null, - EscaperListPopupStep(text, textRange, editor), - UnescaperListPopupStep(text, textRange, editor) - ) { + editor: Editor, + ) : + BaseListPopupStep>( + null, + EscaperListPopupStep(text, textRange, editor), + UnescaperListPopupStep(text, textRange, editor), + ) { override fun hasSubstep(baseListPopupStep: EscaperUnescaperListPopupStep<*>): Boolean = true - override fun getTextFor(baseListPopupStep: EscaperUnescaperListPopupStep<*>): String = baseListPopupStep.actionName + override fun getTextFor(baseListPopupStep: EscaperUnescaperListPopupStep<*>): String = + baseListPopupStep.actionName - override fun onChosen(baseListPopupStep: EscaperUnescaperListPopupStep<*>, finalChoice: Boolean): PopupStep<*> = - baseListPopupStep + override fun onChosen( + baseListPopupStep: EscaperUnescaperListPopupStep<*>, + finalChoice: Boolean, + ): PopupStep<*> = baseListPopupStep } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // interface EscaperUnescaperListPopupStep : PopupStep { val actionName: String } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class EscaperListPopupStep( private val text: String, private val textRange: TextRange, - private val editor: Editor + private val editor: Editor, ) : BaseListPopupStep(null, commonEscaper), EscaperUnescaperListPopupStep { override val actionName: String = "Escape" @@ -91,13 +96,14 @@ abstract class EscapeUnescapeIntentionAction : IntentionAction, LowPriorityActio } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class UnescaperListPopupStep( private val text: String, private val textRange: TextRange, - private val editor: Editor - ) : BaseListPopupStep(null, commonUnescaper), EscaperUnescaperListPopupStep { + private val editor: Editor, + ) : + BaseListPopupStep(null, commonUnescaper), EscaperUnescaperListPopupStep { override val actionName: String = "Unescape" @@ -109,6 +115,6 @@ abstract class EscapeUnescapeIntentionAction : IntentionAction, LowPriorityActio } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/TextCaseConverterIntentionAction.kt b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/TextCaseConverterIntentionAction.kt similarity index 82% rename from modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/TextCaseConverterIntentionAction.kt rename to modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/TextCaseConverterIntentionAction.kt index 3abd054d..1dd4fb50 100644 --- a/modules/common/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/common/tool/editor/intention/TextCaseConverterIntentionAction.kt +++ b/modules/tools/editor/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/TextCaseConverterIntentionAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.common.tool.editor.intention +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention import com.intellij.codeInsight.intention.IntentionAction import com.intellij.codeInsight.intention.LowPriorityAction @@ -16,14 +16,17 @@ import dev.turingcomplete.intellijdevelopertoolsplugin.common.TextCaseUtils.allT import dev.turingcomplete.textcaseconverter.TextCase abstract class TextCaseConverterIntentionAction : IntentionAction, LowPriorityAction { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // final override fun startInWriteAction() = false final override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?) = - editor != null && file != null && editor.document.isWritable && getSourceText(editor, file) != null + editor != null && + file != null && + editor.document.isWritable && + getSourceText(editor, file) != null final override fun invoke(project: Project, editor: Editor?, file: PsiFile?) { if (editor == null || file == null) { @@ -41,13 +44,13 @@ abstract class TextCaseConverterIntentionAction : IntentionAction, LowPriorityAc abstract fun getSourceText(editor: Editor, file: PsiFile): Pair? - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class TextCaseListPopupStep( private val editor: Editor, private val text: String, - private val textRange: TextRange + private val textRange: TextRange, ) : BaseListPopupStep("Select Target Text Case", allTextCases) { override fun getTextFor(textCase: TextCase): String = textCase.example() @@ -63,5 +66,5 @@ abstract class TextCaseConverterIntentionAction : IntentionAction, LowPriorityAc } } - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/after.java.template b/modules/tools/editor/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/after.java.template similarity index 100% rename from src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/after.java.template rename to modules/tools/editor/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/after.java.template diff --git a/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/before.java.template b/modules/tools/editor/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/before.java.template similarity index 100% rename from src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/before.java.template rename to modules/tools/editor/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/before.java.template diff --git a/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/description.html b/modules/tools/editor/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/description.html similarity index 100% rename from src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/description.html rename to modules/tools/editor/src/main/resources/intentionDescriptions/DataGeneratorIntentionAction/description.html diff --git a/modules/tools/editor/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/MainIntentionDescriptionTest.kt b/modules/tools/editor/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/MainIntentionDescriptionTest.kt new file mode 100644 index 00000000..ee7820a5 --- /dev/null +++ b/modules/tools/editor/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/editor/intention/MainIntentionDescriptionTest.kt @@ -0,0 +1,27 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention + +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.IntentionDescriptionTest +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.TestFactory + +class MainIntentionDescriptionTest : IntentionDescriptionTest() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @TestFactory + override fun `test intention action description HTML file exists`(): List = + super.`do test intention action description HTML file exists`() + + @TestFactory + override fun `test intention action before template file exists`(): List = + super.`do test intention action before template file exists`() + + @TestFactory + override fun `test intention action after template file exists`(): List = + super.`do test intention action after template file exists`() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/build.gradle.kts b/modules/tools/ui/build.gradle.kts new file mode 100644 index 00000000..9e4fccd0 --- /dev/null +++ b/modules/tools/ui/build.gradle.kts @@ -0,0 +1,75 @@ +import org.jetbrains.changelog.Changelog + +plugins { + `java-test-fixtures` + alias(libs.plugins.changelog) +} + +dependencies { + implementation(project(":common")) + implementation(project(":settings")) + + implementation(libs.commons.text) + implementation(libs.commons.codec) + implementation(libs.commons.io) + implementation(libs.commons.compress) + implementation(libs.jose4j) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + implementation(libs.bundles.jackson) + implementation(libs.named.regexp) + implementation(libs.bundles.zxing) + implementation(libs.sql.formatter) + implementation(libs.csscolor4j) + implementation(libs.jfiglet) + implementation(libs.jsonpath) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + implementation(libs.json.schema.validator) { + exclude("org.apache.commons", "commons-lang3") + exclude(group = "org.slf4j", module = "slf4j-api") + } + implementation(libs.bundles.text.case.converter) + implementation(libs.ulid.creator) + implementation(libs.jnanoid) + implementation(libs.uuid.generator) { + exclude(group = "org.slf4j", module = "slf4j-api") + } + + testImplementation(libs.assertj.core) + testImplementation(libs.bundles.junit.implementation) + testRuntimeOnly(libs.bundles.junit.runtime) + testImplementation(testFixtures(project(":common"))) + + testFixturesApi(project(":common")) + testFixturesApi(project(":settings")) +} + +changelog { + path.set(rootProject.file("CHANGELOG.md").path) +} + +val writeChangelogToFileTask = + tasks.register("writeChangelogToFile") { + val generatedResourcesDir = layout.buildDirectory.dir("generated-resources/changelog").get() + outputs.dir(generatedResourcesDir) + + doLast { + val renderResult = + changelog.instance.get().releasedItems.joinToString("\n") { + changelog.renderItem(it, Changelog.OutputType.HTML) + } + val baseDir = generatedResourcesDir.dir("dev/turingcomplete/intellijdevelopertoolsplugin") + file(baseDir).mkdirs() + file(baseDir.file("changelog.html")).writeText(renderResult) + } + } + +sourceSets { main { resources { srcDir(writeChangelogToFileTask.map { it.outputs.files }) } } } + +tasks.withType(org.jetbrains.changelog.tasks.InitializeChangelogTask::class).configureEach { + enabled = false +} +tasks.withType(org.jetbrains.changelog.tasks.PatchChangelogTask::class).configureEach { + enabled = false +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/ObjectMapperService.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/ObjectMapperService.kt new file mode 100644 index 00000000..f6f73888 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/ObjectMapperService.kt @@ -0,0 +1,173 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.StreamReadFeature +import com.fasterxml.jackson.core.json.JsonReadFeature +import com.fasterxml.jackson.core.json.JsonWriteFeature +import com.fasterxml.jackson.core.util.DefaultIndenter +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter +import com.fasterxml.jackson.core.util.Separators +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper +import com.fasterxml.jackson.dataformat.toml.TomlMapper +import com.fasterxml.jackson.dataformat.xml.XmlMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings + +@Service(Service.Level.APP) +class ObjectMapperService { + // -- Properties ---------------------------------------------------------- // + + private var jsonMapper: JsonMapper? = null + private var lastJsonHandlingModificationsCounter: Int = 0 + + private val yamlMapper: YAMLMapper = YAMLMapper() + private val xmlMapper: XmlMapper = XmlMapper() + private val tomlMapper: TomlMapper = TomlMapper() + private val javaPropsMapper: JavaPropsMapper = JavaPropsMapper() + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun jsonMapper(): JsonMapper { + if ( + jsonMapper == null || + lastJsonHandlingModificationsCounter != + DeveloperToolsApplicationSettings.jsonHandling.modificationsCounter + ) { + jsonMapper = createJsonMapper() + lastJsonHandlingModificationsCounter = + DeveloperToolsApplicationSettings.jsonHandling.modificationsCounter + } + + return jsonMapper!! + } + + fun yamlMapper(): YAMLMapper = yamlMapper + + fun xmlMapper(): XmlMapper = xmlMapper + + fun tomlMapper(): TomlMapper = tomlMapper + + fun javaPropsMapper(): JavaPropsMapper = javaPropsMapper + + fun prettyPrintJson(jsonNode: JsonNode): String { + if (jsonNode.isMissingNode) { + return "" + } + + return jsonMapper().writerWithDefaultPrettyPrinter().writeValueAsString(jsonNode) + } + + // -- Private Methods ----------------------------------------------------- // + + fun createJsonMapper(): JsonMapper { + val settings = DeveloperToolsApplicationSettings.jsonHandling + + return JsonMapper.builder() + .apply { + enable(StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION) + + // Writing settings + if (settings.writeQuoteFieldNames.get()) { + enable(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature()) + } else { + disable(JsonWriteFeature.QUOTE_FIELD_NAMES.mappedFeature()) + } + + if (settings.writeNanAsStrings.get()) { + enable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS) + } else { + disable(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS) + } + + if (settings.writeNumbersAsStrings.get()) { + enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS.mappedFeature()) + } else { + disable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS.mappedFeature()) + } + + if (settings.writeEscapeNonAscii.get()) { + enable(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature()) + } else { + disable(JsonWriteFeature.ESCAPE_NON_ASCII.mappedFeature()) + } + + if (settings.writeHexUpperCase.get()) { + enable(JsonWriteFeature.WRITE_HEX_UPPER_CASE.mappedFeature()) + } else { + disable(JsonWriteFeature.WRITE_HEX_UPPER_CASE.mappedFeature()) + } + + val indent = " ".repeat(settings.writeIntentionSpaces.get()) + val lineSeparator = System.lineSeparator() + val indenter = DefaultIndenter(indent, lineSeparator) + defaultPrettyPrinter( + DefaultPrettyPrinter() + .withObjectIndenter(indenter) + .withArrayIndenter(indenter) + .withSeparators( + Separators.createDefaultInstance() + .withObjectFieldValueSpacing(settings.writeSpacingObjectFieldValue.get()) + .withObjectEntrySpacing(settings.writeSpacingObjectEntry.get()) + .withArrayValueSpacing(settings.writeSpacingArrayValue.get()) + ) + ) + + // Reading settings + if (settings.readAllowJavaComments.get()) { + enable(JsonReadFeature.ALLOW_JAVA_COMMENTS.mappedFeature()) + } + if (settings.readAllowYamlComments.get()) { + enable(JsonReadFeature.ALLOW_YAML_COMMENTS.mappedFeature()) + } + if (settings.readAllowSingleQuotes.get()) { + enable(JsonReadFeature.ALLOW_SINGLE_QUOTES.mappedFeature()) + } + if (settings.readAllowUnquotedFieldNames.get()) { + enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES.mappedFeature()) + } + if (settings.readAllowUnescapedControlChars.get()) { + enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) + } + if (settings.readAllowBackslashEscapingAnyCharacter.get()) { + enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature()) + } + if (settings.readAllowLeadingZerosForNumbers.get()) { + enable(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS.mappedFeature()) + } + if (settings.readAllowLeadingPlusSignForNumbers.get()) { + enable(JsonReadFeature.ALLOW_LEADING_PLUS_SIGN_FOR_NUMBERS.mappedFeature()) + } + if (settings.readAllowLeadingDecimalPointForNumbers.get()) { + enable(JsonReadFeature.ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS.mappedFeature()) + } + if (settings.readAllowTrailingDecimalPointForNumbers.get()) { + enable(JsonReadFeature.ALLOW_TRAILING_DECIMAL_POINT_FOR_NUMBERS.mappedFeature()) + } + if (settings.readAllowNonNumericNumbers.get()) { + enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS) + } + if (settings.readAllowMissingValues.get()) { + enable(JsonReadFeature.ALLOW_MISSING_VALUES.mappedFeature()) + } + if (settings.readAllowTrailingComma.get()) { + enable(JsonReadFeature.ALLOW_TRAILING_COMMA.mappedFeature()) + } + } + .build() + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + val instance: ObjectMapperService + get() = service() + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperToolsPluginProjectActivity.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperToolsPluginProjectActivity.kt new file mode 100644 index 00000000..9b51e194 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperToolsPluginProjectActivity.kt @@ -0,0 +1,31 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.AddOpenMainDialogActionToMainToolbarTask + +class DeveloperToolsPluginProjectActivity : ProjectActivity { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override suspend fun execute(project: Project) { + if ( + DeveloperToolsApplicationSettings.generalSettings.addOpenMainDialogActionToMainToolbar.get() + ) { + val addOpenMainDialogActionToMainToolbarTask = + AddOpenMainDialogActionToMainToolbarTask.createIfAvailable() + if (addOpenMainDialogActionToMainToolbarTask != null) { + ApplicationManager.getApplication().invokeLater { + addOpenMainDialogActionToMainToolbarTask.run() + } + } + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiTool.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiTool.kt similarity index 66% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiTool.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiTool.kt index 68df326a..097a9de0 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiTool.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiTool.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.main +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider @@ -10,14 +10,15 @@ import com.intellij.ui.ScrollPaneFactory.createScrollPane import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.panel import com.intellij.util.ui.JBEmptyBorder +import com.intellij.util.ui.JBInsets +import com.intellij.util.ui.JBUI import com.intellij.util.ui.components.BorderLayoutPanel import java.awt.Dimension import javax.swing.JComponent -abstract class DeveloperUiTool( - protected val parentDisposable: Disposable -) : DataProvider, Disposable { - // -- Properties -------------------------------------------------------------------------------------------------- // +abstract class DeveloperUiTool(protected val parentDisposable: Disposable) : + DataProvider, Disposable { + // -- Properties ---------------------------------------------------------- // private lateinit var component: DialogPanel private val validationListeners = mutableSetOf<(List) -> Unit>() @@ -26,13 +27,11 @@ abstract class DeveloperUiTool( protected var wrapComponentInScrollPane = true - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // fun createComponent(): JComponent { - component = panel { - buildUi() - } + component = panel { buildUi() } // This prevents the `Editor` from increasing the size of the dialog if the // to display all the text on the screen instead of using scrollbars. The // reason for this behavior is that the UI DSL always sets the minimum size @@ -45,15 +44,16 @@ abstract class DeveloperUiTool( it.registerValidators(parentDisposable) } - var wrapper: JComponent = object : BorderLayoutPanel(), DataProvider { + var wrapper: JComponent = + object : BorderLayoutPanel(), DataProvider { - init { - border = JBEmptyBorder(12, 16, 16, 16) - addToCenter(component) - } + init { + border = JBEmptyBorder(wrapperInsets()) + addToCenter(component) + } - override fun getData(dataId: String): Any? = this@DeveloperUiTool.getData(dataId) - } + override fun getData(dataId: String): Any? = this@DeveloperUiTool.getData(dataId) + } if (wrapComponentInScrollPane) { wrapper = createScrollPane(wrapper, true) @@ -64,6 +64,8 @@ abstract class DeveloperUiTool( return wrapper } + open fun wrapperInsets(): JBInsets = JBUI.insets(12, 16, 16, 16) + abstract fun Panel.buildUi() open fun afterBuildUi() { @@ -96,10 +98,12 @@ abstract class DeveloperUiTool( // Override if needed } - fun validate(): List { - val result = findComponentsOfType(component, DialogPanel::class.java).flatMap { - it.validateAll() - }.toList() + fun validate(onlyVisibleAndEnabled: Boolean = false): List { + val result = + findComponentsOfType(component, DialogPanel::class.java) + .filter { !onlyVisibleAndEnabled || (it.isVisible && it.isEnabled) } + .flatMap { it.validateAll() } + .toList() validationListeners.forEach { it(result) } return result } @@ -108,24 +112,22 @@ abstract class DeveloperUiTool( validationListeners.add(listener) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { /** - * If [com.intellij.ui.dsl.builder.Cell.validationOnApply] gets called and - * there is no [com.intellij.ui.dsl.builder.Cell.validationRequestor] - * registered, IntelliJ will log the warning `Please, install - * Cell.validationRequestor`. To circumvent this, we set a dummy requestor - * without any functionally. + * If [com.intellij.ui.dsl.builder.Cell.validationOnApply] gets called and there is no + * [com.intellij.ui.dsl.builder.Cell.validationRequestor] registered, IntelliJ will log the + * warning `Please, install Cell.validationRequestor`. To circumvent this, we set a dummy + * requestor without any functionally. * - * Future todos: The validation mechanism of the new UI has been further - * developed since the initial creation of the plugin. The current mechanism - * in [DeveloperUiTool.validate], may no longer be necessary and may be better - * solved with on-board resources. + * Future todos: The validation mechanism of the new UI has been further developed since the + * initial creation of the plugin. The current mechanism in [DeveloperUiTool.validate], may no + * longer be necessary and may be better solved with on-board resources. */ val DUMMY_DIALOG_VALIDATION_REQUESTOR = DialogValidationRequestor { _, _ -> } } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolContext.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolContext.kt new file mode 100644 index 00000000..7ac76585 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolContext.kt @@ -0,0 +1,11 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base + +/** Context information from the extension point in the `plugin.xml`. */ +data class DeveloperUiToolContext(val id: String, val prioritizeVerticalLayout: Boolean) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolFactory.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolFactory.kt similarity index 62% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolFactory.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolFactory.kt index 3d01d3b5..5cdb5281 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolFactory.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolFactory.kt @@ -1,23 +1,23 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.main +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration interface DeveloperUiToolFactory { - // -- Properties -------------------------------------------------------------------------------------------------- // - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // fun getDeveloperUiToolPresentation(): DeveloperUiToolPresentation fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> T)? - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolFactoryEp.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolFactoryEp.kt new file mode 100644 index 00000000..bea5d907 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolFactoryEp.kt @@ -0,0 +1,36 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base + +import com.intellij.openapi.extensions.CustomLoadingExtensionPointBean +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.RequiredElement +import com.intellij.util.xmlb.annotations.Attribute + +class DeveloperUiToolFactoryEp> : + CustomLoadingExtensionPointBean() { + // -- Properties ---------------------------------------------------------- // + + @Attribute("id") @RequiredElement lateinit var id: String + + @Attribute("implementationClass") @RequiredElement lateinit var implementationClass: String + + @Attribute("preferredSelected") var preferredSelected: Boolean = false + + @Attribute("groupId") var groupId: String? = null + + @Attribute("internalTool") var internalTool: Boolean = false + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun getImplementationClassName(): String = implementationClass + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + val EP_NAME: ExtensionPointName>> = + ExtensionPointName.create("dev.turingcomplete.intellijdevelopertoolsplugins.developerUiTool") + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolGroup.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolGroup.kt similarity index 58% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolGroup.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolGroup.kt index 0325c537..fa7949e7 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolGroup.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolGroup.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.main +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.extensions.RequiredElement @@ -6,11 +6,9 @@ import com.intellij.util.xmlb.annotations.Attribute import org.jetbrains.annotations.Nls class DeveloperUiToolGroup { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // - @Attribute("id") - @RequiredElement - lateinit var id: String + @Attribute("id") @RequiredElement lateinit var id: String @Attribute("menuTitle") @RequiredElement @@ -22,21 +20,21 @@ class DeveloperUiToolGroup { @Nls(capitalization = Nls.Capitalization.Title) lateinit var detailTitle: String - @Attribute("initiallyExpanded") - var initiallyExpanded: Boolean? = false + @Attribute("initiallyExpanded") var initiallyExpanded: Boolean? = false - @Attribute("weight") - var weight: Int? = 1 + @Attribute("weight") var weight: Int? = 1 - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { val EP_NAME: ExtensionPointName = - ExtensionPointName.create("dev.turingcomplete.intellijdevelopertoolsplugins.developerUiToolGroup") + ExtensionPointName.create( + "dev.turingcomplete.intellijdevelopertoolsplugins.developerUiToolGroup" + ) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolPresentation.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolPresentation.kt similarity index 64% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolPresentation.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolPresentation.kt index 1e7e5ab5..e7d08cda 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolPresentation.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/base/DeveloperUiToolPresentation.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.main +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base import com.intellij.ide.BrowserUtil import com.intellij.openapi.ui.popup.Balloon @@ -7,44 +7,34 @@ import com.intellij.ui.JBColor import com.intellij.ui.awt.RelativePoint import com.intellij.ui.dsl.builder.panel import com.intellij.util.ui.UIUtil -import org.jetbrains.annotations.Nls import javax.swing.JComponent +import org.jetbrains.annotations.Nls data class DeveloperUiToolPresentation( - @Nls(capitalization = Nls.Capitalization.Title) - val menuTitle: String, - - @Nls(capitalization = Nls.Capitalization.Title) - val groupedMenuTitle: String = menuTitle, - - @Nls(capitalization = Nls.Capitalization.Title) - val contentTitle: String, - - @Nls(capitalization = Nls.Capitalization.Sentence) - val description: Description? = null + @Nls(capitalization = Nls.Capitalization.Title) val menuTitle: String, + @Nls(capitalization = Nls.Capitalization.Title) val groupedMenuTitle: String = menuTitle, + @Nls(capitalization = Nls.Capitalization.Title) val contentTitle: String, + @Nls(capitalization = Nls.Capitalization.Sentence) val description: Description? = null, ) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // interface Description { fun show(parentComponent: JComponent) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class ContextHelpDescription(private val description: String) : Description { override fun show(parentComponent: JComponent) { - val panel = panel { - row { - label("$description") - } - } - JBPopupFactory.getInstance().createBalloonBuilder(panel) + val panel = panel { row { label("$description") } } + JBPopupFactory.getInstance() + .createBalloonBuilder(panel) .setDialogMode(true) .setFillColor(UIUtil.getPanelBackground()) .setBorderColor(JBColor.border()) @@ -58,7 +48,7 @@ data class DeveloperUiToolPresentation( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class ExternalLinkDescription(private val url: String) : Description { @@ -67,7 +57,7 @@ data class DeveloperUiToolPresentation( } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @@ -75,4 +65,4 @@ data class DeveloperUiToolPresentation( fun externalLink(url: String): Description = ExternalLinkDescription(url) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/DeveloperToolEditor.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AdvancedEditor.kt similarity index 58% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/DeveloperToolEditor.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AdvancedEditor.kt index 4bc2d4c0..5eb06967 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/DeveloperToolEditor.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AdvancedEditor.kt @@ -1,6 +1,4 @@ -@file:Suppress("UnstableApiUsage") - -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.application.options.CodeStyle import com.intellij.icons.AllIcons @@ -49,14 +47,15 @@ import com.intellij.ui.layout.ValidationInfoBuilder import com.intellij.util.ui.JBUI import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils.actionsPopup -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils.dumbAwareAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.message.GeneralBundle -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings import dev.turingcomplete.intellijdevelopertoolsplugin.common.EditorUtils.getEditor -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.actionsPopup +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.dumbAwareAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.GeneralBundle import java.awt.datatransfer.StringSelection import java.nio.file.Files import java.nio.file.StandardOpenOption @@ -66,7 +65,7 @@ import javax.swing.JComponent import javax.swing.ScrollPaneConstants import kotlin.math.max -internal class DeveloperToolEditor( +class AdvancedEditor( private val id: String, private val context: DeveloperUiToolContext, private val configuration: DeveloperToolConfiguration, @@ -79,13 +78,28 @@ internal class DeveloperToolEditor( private val diffSupport: DiffSupport? = null, private val supportsExpand: Boolean = true, private val minimumSizeHeight: Int = DEFAULT_MINIMUM_SIZE_HEIGHT, - private val fixedEditorSoftWraps: Boolean? = null + private val fixedEditorSoftWraps: Boolean? = null, ) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // - private var softWraps = configuration.register("${context.id}-${id}-softWraps", fixedEditorSoftWraps ?: DeveloperToolsApplicationSettings.instance.editorSoftWraps, CONFIGURATION) - private var showSpecialCharacters = configuration.register("${context.id}-${id}-showSpecialCharacters", DeveloperToolsApplicationSettings.instance.editorShowSpecialCharacters, CONFIGURATION) - private var showWhitespaces = configuration.register("${context.id}-${id}-showWhitespaces", DeveloperToolsApplicationSettings.instance.editorShowWhitespaces, CONFIGURATION) + private var softWraps = + configuration.register( + key = "${id}-editor-softWraps", + defaultValue = fixedEditorSoftWraps ?: generalSettings.editorSoftWraps.get(), + propertyType = CONFIGURATION, + ) + private var showSpecialCharacters = + configuration.register( + key = "${id}-editor-showSpecialCharacters", + defaultValue = generalSettings.editorShowSpecialCharacters.get(), + propertyType = CONFIGURATION, + ) + private var showWhitespaces = + configuration.register( + key = "${id}-editor-showWhitespaces", + defaultValue = generalSettings.editorShowWhitespaces.get(), + propertyType = CONFIGURATION, + ) private var onTextChangeFromUi = mutableListOf<((String) -> Unit)>() private var onFocusGained: (() -> Unit)? = null @@ -109,7 +123,7 @@ internal class DeveloperToolEditor( val component: JComponent by lazy { createComponent() } - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { textProperty.afterChangeConsumeEvent(parentDisposable) { event -> @@ -149,20 +163,20 @@ internal class DeveloperToolEditor( } } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // - fun onTextChangeFromUi(changeListener: ((String) -> Unit)): DeveloperToolEditor { + fun onTextChangeFromUi(changeListener: ((String) -> Unit)): AdvancedEditor { onTextChangeFromUi.add(changeListener) return this } - fun onFocusGained(changeListener: () -> Unit): DeveloperToolEditor { + fun onFocusGained(changeListener: () -> Unit): AdvancedEditor { onFocusGained = changeListener return this } @Suppress("unused") - fun onFocusLost(changeListener: () -> Unit): DeveloperToolEditor { + fun onFocusLost(changeListener: () -> Unit): AdvancedEditor { onFocusLost = changeListener return this } @@ -173,7 +187,12 @@ internal class DeveloperToolEditor( title?.let { addToTop(JBLabel("$it ${editorMode.title}:")) } editor.component.border = ValidationResultBorder(editor.component, editor.contentComponent) - val editorComponent = editor.component.wrapWithToolBar(DeveloperToolEditor::class.java.simpleName, createActions(), ToolBarPlace.RIGHT) + val editorComponent = + editor.component.wrapWithToolBar( + AdvancedEditor::class.java.simpleName, + createActions(), + ToolBarPlace.RIGHT, + ) addToCenter(editorComponent) // This prevents the `Editor` from increasing the size of the dialog if // the text in the editor is larger than the preferred height on the @@ -182,13 +201,16 @@ internal class DeveloperToolEditor( minimumSize = JBUI.size(0, minimumSizeHeight) } - override fun getData(dataId: String): Any? = when { - CommonDataKeys.EDITOR.`is`(dataId) -> editor - else -> null - } + override fun getData(dataId: String): Any? = + when { + CommonDataKeys.EDITOR.`is`(dataId) -> editor + else -> null + } } - fun bindValidator(validation: ValidationInfoBuilder.(T) -> ValidationInfo?): ValidationInfoBuilder.(T) -> ValidationInfo? = { + fun bindValidator( + validation: ValidationInfoBuilder.(T) -> ValidationInfo? + ): ValidationInfoBuilder.(T) -> ValidationInfo? = { validation(it)?.forComponent(editor.component) } @@ -209,108 +231,134 @@ internal class DeveloperToolEditor( groupId: String = "other", gutterIconRenderer: GutterIconRenderer? = null, ) { - val rangeHighlighter = editor.markupModel.addRangeHighlighter( - textRange.startOffset, - textRange.endOffset, - layer, - textAttributes, - HighlighterTargetArea.EXACT_RANGE - ) + val rangeHighlighter = + editor.markupModel.addRangeHighlighter( + textRange.startOffset, + textRange.endOffset, + layer, + textAttributes, + HighlighterTargetArea.EXACT_RANGE, + ) rangeHighlighter.gutterIconRenderer = gutterIconRenderer rangeHighlighters.computeIfAbsent(groupId) { mutableListOf() }.add(rangeHighlighter) } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // - private fun createActions(): ActionGroup = DefaultActionGroup().apply { - add(CopyContentAction()) - if (editorMode.editable) { - add(ClearContentAction()) - } + private fun createActions(): ActionGroup = + DefaultActionGroup().apply { + add(CopyContentAction()) + if (editorMode.editable) { + add(ClearContentAction()) + } - addSeparator() - - val settingsActions = mutableListOf().apply { - add(SimpleToggleAction( - text = GeneralBundle.message("editor.soft-wrap"), - icon = AllIcons.Actions.ToggleSoftWrap, - isSelected = { softWraps.get() }, - setSelected = { softWraps.set(it) }, - isEnabled = fixedEditorSoftWraps?.let { { fixedEditorSoftWraps } } - )) - add(SimpleToggleAction( - text = GeneralBundle.message("editor.show-special-characters"), - icon = null, - isSelected = { showSpecialCharacters.get() }, - setSelected = { showSpecialCharacters.set(it) } - )) - add(SimpleToggleAction( - text = GeneralBundle.message("editor.show-whitespaces"), - icon = null, - isSelected = { showWhitespaces.get() }, - setSelected = { showWhitespaces.set(it) } - )) - } - add( - actionsPopup( - title = GeneralBundle.message("editor.settings"), - icon = AllIcons.General.Settings, - actions = settingsActions + addSeparator() + + val settingsActions = + mutableListOf().apply { + add( + SimpleToggleAction( + text = GeneralBundle.message("advanced-editor.soft-wrap"), + icon = AllIcons.Actions.ToggleSoftWrap, + isSelected = { softWraps.get() }, + setSelected = { softWraps.set(it) }, + isEnabled = fixedEditorSoftWraps?.let { { fixedEditorSoftWraps } }, + ) + ) + add( + SimpleToggleAction( + text = GeneralBundle.message("advanced-editor.show-special-characters"), + icon = null, + isSelected = { showSpecialCharacters.get() }, + setSelected = { showSpecialCharacters.set(it) }, + ) + ) + add( + SimpleToggleAction( + text = GeneralBundle.message("advanced-editor.show-whitespaces"), + icon = null, + isSelected = { showWhitespaces.get() }, + setSelected = { showWhitespaces.set(it) }, + ) + ) + } + add( + actionsPopup( + title = GeneralBundle.message("advanced-editor.settings"), + icon = AllIcons.General.Settings, + actions = settingsActions, + ) ) - ) - addSeparator() + addSeparator() - val additionalActions = mutableListOf().apply { - addAll(createDiffAction()) - add(Separator.getInstance()) - add(SaveContentToFileAction()) - if (editorMode.editable) { - add(OpenContentFromFileAction()) - } - if (supportsExpand) { - add(Separator.getInstance()) - add(ExpandEditorAction(this@DeveloperToolEditor)) - } - } - add( - actionsPopup( - title = GeneralBundle.message("editor.additional-actions"), - icon = AllIcons.Actions.MoreHorizontal, - actions = additionalActions + val additionalActions = + mutableListOf().apply { + addAll(createDiffAction()) + add(Separator.getInstance()) + add(SaveContentToFileAction()) + if (editorMode.editable) { + add(OpenContentFromFileAction()) + } + if (supportsExpand) { + add(Separator.getInstance()) + add(ExpandEditorAction(this@AdvancedEditor)) + } + } + add( + actionsPopup( + title = GeneralBundle.message("advanced-editor.additional-actions"), + icon = AllIcons.Actions.MoreHorizontal, + actions = additionalActions, + ) ) - ) - } + } private fun createDiffAction(): List { val actions = mutableListOf() - val firstTitle = title ?: GeneralBundle.message("editor.diff-no-title-fallback") - - actions.add(dumbAwareAction(GeneralBundle.message("editor.show-diff-with-clipboard"), AllIcons.Actions.DiffWithClipboard) { e -> - val editor = e.getEditor() - val firstText = runReadAction { editor.document.text } - UiUtils.showDiffDialog( - title = GeneralBundle.message("editor.show-diff-with-clipboard"), - firstTitle = firstTitle, - secondTitle = GeneralBundle.message("editor.clipboard"), - firstText = firstText, - secondText = ClipboardUtil.getTextInClipboard() ?: "" - ) - }) + val firstTitle = title ?: GeneralBundle.message("advanced-editor.diff-no-title-fallback") - diffSupport?.let { - actions.add(dumbAwareAction(GeneralBundle.message("editor.show-diff-with-title", it.secondTitle), AllIcons.Actions.Diff) { e -> + actions.add( + dumbAwareAction( + GeneralBundle.message("advanced-editor.show-diff-with-clipboard"), + AllIcons.Actions.DiffWithClipboard, + ) { e -> val editor = e.getEditor() val firstText = runReadAction { editor.document.text } UiUtils.showDiffDialog( - title = GeneralBundle.message("editor.show-diff-with-title", it.secondTitle), + title = GeneralBundle.message("advanced-editor.show-diff-with-clipboard"), firstTitle = firstTitle, - secondTitle = it.secondTitle, + secondTitle = GeneralBundle.message("advanced-editor.clipboard"), firstText = firstText, - secondText = it.secondText() + secondText = ClipboardUtil.getTextInClipboard() ?: "", ) - }) + } + ) + + if (diffSupport != null) { + actions.add( + dumbAwareAction( + title = + GeneralBundle.message("advanced-editor.show-diff-with-title", diffSupport.secondTitle), + icon = AllIcons.Actions.Diff, + isEnabledAndVisible = diffSupport.enabled?.let { { it.get() } } ?: { true }, + ) { e -> + val editor = e.getEditor() + val firstText = runReadAction { editor.document.text } + UiUtils.showDiffDialog( + title = + GeneralBundle.message( + "advanced-editor.show-diff-with-title", + diffSupport.secondTitle, + ), + firstTitle = firstTitle, + secondTitle = diffSupport.secondTitle, + firstText = firstText, + secondText = diffSupport.secondText(), + ) + } + ) } return actions @@ -318,13 +366,14 @@ internal class DeveloperToolEditor( private fun createEditor(): EditorEx { val editorFactory = EditorFactory.getInstance() - val document = (editorFactory as EditorFactoryImpl).createDocument(textProperty.get(), true, false) - val editor = if (editorMode.editable) { - editorFactory.createEditor(document, project) as EditorEx - } - else { - editorFactory.createViewer(document, project) as EditorEx - } + val document = + (editorFactory as EditorFactoryImpl).createDocument(textProperty.get(), true, false) + val editor = + if (editorMode.editable) { + editorFactory.createEditor(document, project) as EditorEx + } else { + editorFactory.createViewer(document, project) as EditorEx + } editor.putUserData(editorActiveKey, UIUtil.hasFocus(editor.contentComponent)) Disposer.register(parentDisposable) { EditorFactory.getInstance().releaseEditor(editor) } @@ -352,9 +401,7 @@ internal class DeveloperToolEditor( isWhitespacesShown = showWhitespaces.get() isBlinkCaret = editorMode.editable additionalLinesCount = 0 - project?.let { - setTabSize(CodeStyle.getIndentOptions(it, document).TAB_SIZE) - } + project?.let { setTabSize(CodeStyle.getIndentOptions(it, document).TAB_SIZE) } } } } @@ -364,22 +411,22 @@ internal class DeveloperToolEditor( val isLaFDark = ColorUtil.isDark(UIUtil.getPanelBackground()) val isEditorDark = EditorColorsManager.getInstance().isDarkEditor - colorsScheme = if (isLaFDark == isEditorDark) { - EditorColorsManager.getInstance().globalScheme - } - else { - EditorColorsManager.getInstance().schemeForCurrentUITheme - } + colorsScheme = + if (isLaFDark == isEditorDark) { + EditorColorsManager.getInstance().globalScheme + } else { + EditorColorsManager.getInstance().schemeForCurrentUITheme + } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private inner class TextChangeListener : DocumentListener { override fun documentChanged(event: DocumentEvent) { val currentText = event.document.text - textProperty.set(value = currentText, changeId = TEXT_CHANGE_FROM_DOCUMENT_LISTENER) + textProperty.set(newValue = currentText, changeId = TEXT_CHANGE_FROM_DOCUMENT_LISTENER) if (editor.getUserData(editorActiveKey)!!) { onTextChangeFromUi.forEach { it(currentText) } @@ -387,7 +434,7 @@ internal class DeveloperToolEditor( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private inner class FocusListener : FocusChangeListener { @@ -404,10 +451,14 @@ internal class DeveloperToolEditor( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class ClearContentAction - : DumbAwareAction(GeneralBundle.message("editor.clear-content-action-title"), null, AllIcons.Actions.GC) { + private class ClearContentAction : + DumbAwareAction( + GeneralBundle.message("advanced-editor.clear-content-action-title"), + null, + AllIcons.Actions.GC, + ) { override fun actionPerformed(e: AnActionEvent) { val editor = e.getEditor() @@ -421,10 +472,14 @@ internal class DeveloperToolEditor( override fun getActionUpdateThread() = ActionUpdateThread.BGT } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class CopyContentAction - : DumbAwareAction(GeneralBundle.message("editor.copy-to-clipboard-action-title"), null, AllIcons.Actions.Copy) { + private class CopyContentAction : + DumbAwareAction( + GeneralBundle.message("advanced-editor.copy-to-clipboard-action-title"), + null, + AllIcons.Actions.Copy, + ) { override fun actionPerformed(e: AnActionEvent) { val editor = e.getEditor() @@ -435,44 +490,57 @@ internal class DeveloperToolEditor( override fun getActionUpdateThread() = ActionUpdateThread.BGT } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class SaveContentToFileAction : DumbAwareAction( - GeneralBundle.message("editor.save-to-file-action-title"), - null, - AllIcons.Actions.MenuSaveall - ) { + private class SaveContentToFileAction : + DumbAwareAction( + GeneralBundle.message("advanced-editor.save-to-file-action-title"), + null, + AllIcons.Actions.MenuSaveall, + ) { override fun actionPerformed(e: AnActionEvent) { val editor = e.getEditor() - val fileSaverDescriptor = FileSaverDescriptor(GeneralBundle.message("editor.file-saver-description"), "") + val fileSaverDescriptor = + FileSaverDescriptor(GeneralBundle.message("advanced-editor.file-saver-description"), "") val timeStamp = LocalDateTime.now().format(timestampFormat) val defaultFilename = "$timeStamp.txt" FileChooserFactory.getInstance() .createSaveFileDialog(fileSaverDescriptor, e.project) - .save(defaultFilename)?.file?.toPath()?.let { + .save(defaultFilename) + ?.file + ?.toPath() + ?.let { val content = runReadAction { editor.document.text } - Files.writeString(it, content, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING) + Files.writeString( + it, + content, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + ) } } override fun getActionUpdateThread() = ActionUpdateThread.BGT } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class OpenContentFromFileAction : DumbAwareAction( - GeneralBundle.message("editor.open-file-action-title"), - null, - AllIcons.Actions.MenuOpen - ) { + private class OpenContentFromFileAction : + DumbAwareAction( + GeneralBundle.message("advanced-editor.open-file-action-title"), + null, + AllIcons.Actions.MenuOpen, + ) { override fun actionPerformed(e: AnActionEvent) { val editor = e.getEditor() val fileChooserDescriptor = FileChooserDescriptor(true, true, false, false, false, false) FileChooserFactory.getInstance() .createFileChooser(fileChooserDescriptor, e.project, editor.component) - .choose(e.project).firstOrNull()?.let { + .choose(e.project) + .firstOrNull() + ?.let { runWriteAction { editor.putUserData(editorActiveKey, true) editor.document.setText(Files.readString(it.toNioPath())) @@ -484,27 +552,34 @@ internal class DeveloperToolEditor( override fun getActionUpdateThread() = ActionUpdateThread.BGT } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class ExpandEditorAction(private val originalEditor: DeveloperToolEditor) : - AnActionButton(GeneralBundle.message("editor.expand-editor-action-title"), null, AllIcons.Actions.MoveToWindow), DumbAware { + private class ExpandEditorAction(private val originalEditor: AdvancedEditor) : + AnActionButton( + GeneralBundle.message("advanced-editor.expand-editor-action-title"), + null, + AllIcons.Actions.MoveToWindow, + ), + DumbAware { override fun actionPerformed(e: AnActionEvent) { val expandedText = ValueProperty(originalEditor.text) val expandedEditor = createExpandedEditor(originalEditor, expandedText) - val apply = object: DialogWrapper(originalEditor.component, true) { + val apply = + object : DialogWrapper(originalEditor.component, true) { - init { - setSize(700, 550) - init() + init { + setSize(700, 550) + init() - setOKButtonText(GeneralBundle.message("editor.apply")) - } + setOKButtonText(GeneralBundle.message("advanced-editor.apply")) + } - override fun createCenterPanel(): JComponent = expandedEditor.component + override fun createCenterPanel(): JComponent = expandedEditor.component - override fun getDimensionServiceKey(): String? = ExpandEditorAction::class.java.name - }.showAndGet() + override fun getDimensionServiceKey(): String? = ExpandEditorAction::class.java.name + } + .showAndGet() if (apply) { runWriteAction { @@ -514,8 +589,11 @@ internal class DeveloperToolEditor( } } - private fun createExpandedEditor(originalEditor: DeveloperToolEditor, textProperty: ValueProperty) = - DeveloperToolEditor( + private fun createExpandedEditor( + originalEditor: AdvancedEditor, + textProperty: ValueProperty, + ) = + AdvancedEditor( id = "${originalEditor.id}-expanded", context = originalEditor.context, configuration = originalEditor.configuration, @@ -526,29 +604,31 @@ internal class DeveloperToolEditor( textProperty = textProperty, initialLanguage = originalEditor.initialLanguage, diffSupport = originalEditor.diffSupport, - supportsExpand = false + supportsExpand = false, ) override fun getActionUpdateThread() = ActionUpdateThread.BGT } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + + // -- Inner Type ---------------------------------------------------------- // data class DiffSupport( val title: String, val secondTitle: String, - val secondText: () -> String + val secondText: () -> String, + val enabled: ValueProperty? = null, ) - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // enum class EditorMode(val title: String, val editable: Boolean) { - INPUT(GeneralBundle.message("editor.mode.input"), true), - OUTPUT(GeneralBundle.message("editor.mode.output"), false), - INPUT_OUTPUT(GeneralBundle.message("editor.mode.input-output"), true), + INPUT(GeneralBundle.message("advanced-editor.mode.input"), true), + OUTPUT(GeneralBundle.message("advanced-editor.mode.output"), false), + INPUT_OUTPUT(GeneralBundle.message("advanced-editor.mode.input-output"), true), } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @@ -557,7 +637,7 @@ internal class DeveloperToolEditor( private const val DEFAULT_PREFFERED_SIZE_HEIGHT = 120 private const val DEFAULT_MINIMUM_SIZE_HEIGHT = 50 - private val editorActiveKey = Key("editorActive") + private val editorActiveKey = Key("advanced-editorActive") private val timestampFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-SS") } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/AnActionOptionButton.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AnActionOptionButton.kt similarity index 67% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/AnActionOptionButton.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AnActionOptionButton.kt index d8438e7d..9a038ce5 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/AnActionOptionButton.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AnActionOptionButton.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.ide.DataManager import com.intellij.openapi.actionSystem.ActionUiKind @@ -12,21 +12,21 @@ import java.awt.event.ActionEvent import javax.swing.AbstractAction import javax.swing.JComponent -class AnActionOptionButton(mainAction: AnAction, vararg additionalActions: AnAction) - : JBOptionButton(null, null) { - // -- Companion Object -------------------------------------------------------------------------------------------- // - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // +class AnActionOptionButton(mainAction: AnAction, vararg additionalActions: AnAction) : + JBOptionButton(null, null) { + // -- Companion Object ---------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { action = AnActionWrapper(mainAction, this) options = additionalActions.map { AnActionWrapper(it, this) }.toTypedArray() } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // - private class AnActionWrapper(private val action: AnAction, private val component: JComponent) - : AbstractAction(action.templatePresentation.text, action.templatePresentation.icon) { + private class AnActionWrapper(private val action: AnAction, private val component: JComponent) : + AbstractAction(action.templatePresentation.text, action.templatePresentation.icon) { init { putValue(OptionAction.AN_ACTION, action) @@ -34,12 +34,13 @@ class AnActionOptionButton(mainAction: AnAction, vararg additionalActions: AnAct override fun actionPerformed(e: ActionEvent?) { val context: DataContext = DataManager.getInstance().getDataContext(component) - val event = AnActionEvent.createEvent(action, context, null, "jvmaction", ActionUiKind.NONE, null) + val event = + AnActionEvent.createEvent(action, context, null, "jvmaction", ActionUiKind.NONE, null) ActionUtil.performActionDumbAwareWithCallbacks(action, event) } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // } - diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/AsyncTaskExecutor.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AsyncTaskExecutor.kt similarity index 65% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/AsyncTaskExecutor.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AsyncTaskExecutor.kt index 28f2d7c0..ff5d5f00 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/AsyncTaskExecutor.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/AsyncTaskExecutor.kt @@ -1,9 +1,10 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.openapi.Disposable import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.invokeLater import com.intellij.openapi.util.Disposer +import java.util.concurrent.ConcurrentLinkedQueue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -11,26 +12,25 @@ import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.ConcurrentLinkedQueue class AsyncTaskExecutor( parentDisposable: Disposable, - private val executionThread: ExecutionThread + private val executionThread: ExecutionThread, ) : Disposable { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val taskQueue = ConcurrentLinkedQueue>() private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) var isDisposed: Boolean = false private set - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { Disposer.register(parentDisposable, this) } - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // override fun dispose() { isDisposed = true @@ -49,8 +49,9 @@ class AsyncTaskExecutor( private fun processQueue() { coroutineScope.launch { - while (taskQueue.isNotEmpty()) { - val (task, delayMillis) = taskQueue.poll() + while (true) { + val item = taskQueue.poll() ?: break + val (task, delayMillis) = item if (delayMillis > 0) { delay(delayMillis) } @@ -59,34 +60,31 @@ class AsyncTaskExecutor( } } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private suspend fun executeTask(task: Runnable) { when (executionThread) { - ExecutionThread.POOLED -> withContext(Dispatchers.IO) { - task.run() - } - ExecutionThread.EDT -> withContext(Dispatchers.Main) { - invokeLater(ModalityState.any()) { - task.run() - } - } + ExecutionThread.POOLED -> withContext(Dispatchers.IO) { task.run() } + ExecutionThread.EDT -> + withContext(Dispatchers.Main) { invokeLater(ModalityState.any()) { task.run() } } } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // enum class ExecutionThread { POOLED, - EDT + EDT, } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { - fun onEdt(parentDisposable: Disposable) = AsyncTaskExecutor(parentDisposable, ExecutionThread.EDT) + fun onEdt(parentDisposable: Disposable) = + AsyncTaskExecutor(parentDisposable, ExecutionThread.EDT) - fun onPooled(parentDisposable: Disposable) = AsyncTaskExecutor(parentDisposable, ExecutionThread.POOLED) + fun onPooled(parentDisposable: Disposable) = + AsyncTaskExecutor(parentDisposable, ExecutionThread.POOLED) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/BooleanComponentPredicate.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/BooleanComponentPredicate.kt similarity index 58% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/BooleanComponentPredicate.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/BooleanComponentPredicate.kt index 67846b9a..01f53c8f 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/BooleanComponentPredicate.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/BooleanComponentPredicate.kt @@ -1,24 +1,23 @@ -@file:Suppress("UnstableApiUsage") - -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.openapi.observable.dispatcher.SingleEventDispatcher import com.intellij.ui.layout.ComponentPredicate import kotlin.properties.Delegates class BooleanComponentPredicate(initialValue: Boolean) : ComponentPredicate() { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val changeDispatcher = SingleEventDispatcher.create() - var value: Boolean by Delegates.observable(initialValue) { _, oldValue, newValue -> - if (oldValue != newValue) { - changeDispatcher.fireEvent(newValue) + var value: Boolean by + Delegates.observable(initialValue) { _, oldValue, newValue -> + if (oldValue != newValue) { + changeDispatcher.fireEvent(newValue) + } } - } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun addListener(listener: (Boolean) -> Unit) { changeDispatcher.whenEventHappened(listener) @@ -26,7 +25,7 @@ class BooleanComponentPredicate(initialValue: Boolean) : ComponentPredicate() { override fun invoke(): Boolean = value - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/CopyAction.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/CopyAction.kt similarity index 61% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/CopyAction.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/CopyAction.kt index 45a3a3c0..50f6bfab 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/CopyAction.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/CopyAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.AnActionEvent @@ -7,27 +7,27 @@ import com.intellij.openapi.ide.CopyPasteManager import com.intellij.openapi.project.DumbAwareAction import java.awt.datatransfer.StringSelection -internal class CopyAction(private val contentDataKey: DataKey = CONTENT_DATA_KEY) : +class CopyAction(private val contentDataKey: DataKey = CONTENT_DATA_KEY) : DumbAwareAction( - "Copy to Clipboard", - "Copy the text into the system clipboard", - AllIcons.Actions.Copy + "Copy to Clipboard", + "Copy the text into the system clipboard", + AllIcons.Actions.Copy, ) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun actionPerformed(e: AnActionEvent) { val content = e.getData(contentDataKey) ?: error("Data missing for: ${contentDataKey.name}") CopyPasteManager.getInstance().setContents(StringSelection(content)) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { val CONTENT_DATA_KEY = DataKey.create("content") } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ErrorHolder.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ErrorHolder.kt new file mode 100644 index 00000000..64fb6305 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ErrorHolder.kt @@ -0,0 +1,139 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common + +import com.fasterxml.jackson.core.JacksonException +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.layout.ComponentPredicate +import com.intellij.ui.layout.ValidationInfoBuilder +import javax.swing.JComponent + +class ErrorHolder( + // Icon must be used in a `text()` cell, a `label()` cell will not work. + private val addErrorIconToMessage: Boolean = false, + // Needs to be false if the error is presented in a label + private val surroundMessageWithHtml: Boolean = true, +) { + // -- Properties ---------------------------------------------------------- // + + private val changeListeners = mutableListOf<(List) -> Unit>() + + private var errors = mutableListOf() + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + fun add(prefix: String, error: Throwable) { + errors.add("$prefix ${determineMessage(error)}") + fireChange() + } + + fun add(error: Throwable) { + errors.add(determineMessage(error)) + fireChange() + } + + fun add(message: String) { + errors.add(message) + fireChange() + } + + fun addIfNoErrors(message: String) { + if (errors.isNotEmpty()) { + return + } + errors.add(message) + fireChange() + } + + fun clear() { + errors.clear() + fireChange() + } + + fun isSet() = errors.isNotEmpty() + + fun isNotSet() = errors.isEmpty() + + fun asComponentPredicate() = + object : ComponentPredicate() { + + override fun addListener(listener: (Boolean) -> Unit) { + changeListeners.add { listener(it.isNotEmpty()) } + } + + override fun invoke(): Boolean = errors.isNotEmpty() + } + + fun asValidation( + forComponent: JComponent? = null + ): ValidationInfoBuilder.(T) -> ValidationInfo? { + return { formatErrors()?.let { ValidationInfo(it, forComponent) } } + } + + /** + * Creates a [ObservableMutableProperty] that will return an empty string if there is no error. + * + * The `set()` operation is not supported, but a [com.intellij.ui.dsl.builder.Row.text] requires + * [ObservableMutableProperty] and not only a + * [com.intellij.openapi.observable.properties.ObservableProperty]. + */ + fun asPropertyForTextCell(): ObservableMutableProperty = + object : ObservableMutableProperty { + + override fun afterChange(listener: (String) -> Unit) { + changeListeners.add { listener(formatErrors() ?: "") } + } + + override fun get(): String = formatErrors() ?: "" + + override fun set(value: String) { + throw UnsupportedOperationException() + } + } + + fun catchException(task: () -> Unit) { + try { + clear() + task() + } catch (e: Exception) { + add(e) + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun determineMessage(error: Throwable): String = + when { + error is JacksonException -> "${error.originalMessage}; ${error.location.offsetDescription()}" + error.message != null -> error.message!! + else -> error::class.simpleName ?: "unknown" + } + + private fun formatErrors(): String? = + errors.let { + when (it.size) { + 0 -> null + 1 -> + if (surroundMessageWithHtml) "${formatError(it[0])}" else formatError(it[0]) + else -> + it.joinToString( + separator = "\n", + prefix = + "${if (surroundMessageWithHtml) "" else ""}
    ", + postfix = "
${if (surroundMessageWithHtml) "" else ""}", + ) { error -> + "
  • ${formatError(error)}
  • " + } + } + } + + private fun formatError(message: String) = + "${if (addErrorIconToMessage) " " else ""}$message" + + private fun fireChange() { + changeListeners.forEach { it(errors) } + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/FileDropHandler.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/FileDropHandler.kt new file mode 100644 index 00000000..db88f148 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/FileDropHandler.kt @@ -0,0 +1,86 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common + +import com.intellij.ide.dnd.FileCopyPasteUtil +import com.intellij.openapi.editor.EditorDropHandler +import com.intellij.openapi.fileEditor.impl.EditorWindow +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import java.awt.datatransfer.DataFlavor +import java.awt.datatransfer.Transferable +import java.awt.dnd.DropTargetDragEvent +import java.awt.dnd.DropTargetDropEvent +import java.awt.dnd.DropTargetEvent +import java.awt.dnd.DropTargetListener +import java.nio.file.Path +import javax.swing.JComponent +import javax.swing.TransferHandler + +class FileDropHandler(private val project: Project?, private val openFile: (Path) -> Unit) : + TransferHandler(), EditorDropHandler, DropTargetListener { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun canImport(comp: JComponent?, transferFlavors: Array?): Boolean { + return canHandleDrop0(transferFlavors) + } + + override fun canHandleDrop(transferFlavors: Array): Boolean { + return canHandleDrop0(transferFlavors) + } + + override fun drop(event: DropTargetDropEvent) { + event.acceptDrop(event.dropAction) + handleDrop0(event.transferable) + } + + override fun handleDrop( + transferable: Transferable, + project: Project?, + editorWindow: EditorWindow?, + ) { + handleDrop0(transferable) + } + + override fun importData(comp: JComponent?, transferable: Transferable): Boolean { + return handleDrop0(transferable) + } + + override fun dragEnter(dtde: DropTargetDragEvent?) { + // Nothing to do + } + + override fun dragOver(dtde: DropTargetDragEvent?) { + // Nothing to do + } + + override fun dropActionChanged(dtde: DropTargetDragEvent?) { + // Nothing to do + } + + override fun dragExit(dte: DropTargetEvent?) { + // Nothing to do + } + + private fun canHandleDrop0(transferFlavors: Array?): Boolean { + return transferFlavors != null && FileCopyPasteUtil.isFileListFlavorAvailable(transferFlavors) + } + + private fun handleDrop0(transferable: Transferable): Boolean { + try { + FileCopyPasteUtil.getFiles(transferable) + ?.takeIf { it.size == 1 } + ?.let { + openFile(it[0]) + return true + } + } catch (e: Exception) { + Messages.showErrorDialog(project, "Failed to handle dropped file: ${e.message}.", "Open File") + } + return false + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/FileHandling.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/FileHandling.kt new file mode 100644 index 00000000..54379534 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/FileHandling.kt @@ -0,0 +1,172 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common + +import com.intellij.icons.AllIcons +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.components.JBLabel +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.ThreadingAssertions.assertBackgroundThread +import com.intellij.util.text.DateFormatUtil +import com.intellij.util.ui.UIUtil +import com.intellij.util.ui.components.BorderLayoutPanel +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.toHexString +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool.Companion.DUMMY_DIALOG_VALIDATION_REQUESTOR +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.GeneralBundle +import java.awt.dnd.DropTarget +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import javax.swing.JComponent +import javax.swing.SwingConstants + +class FileHandling( + val project: Project?, + val file: ValueProperty = ValueProperty(""), + val writeFormat: ValueProperty = ValueProperty(WriteFormat.BINARY), + val supportsWrite: Boolean, +) { + // -- Properties ---------------------------------------------------------- // + + private val lastWriteInformation = + ValueProperty(GeneralBundle.message("file-handling.last-write.never")) + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun crateComponent(errorHolder: ErrorHolder): JComponent = panel { + row { + // Allow the selection of folders so that the user can manually extend the path with a file + // name, of the file does not exist yet. + val fileChooserDescriptor = + FileChooserDescriptor(true, true, false, false, false, false) + .withTitle(GeneralBundle.message("file-handling.file-chooser.title")) + textFieldWithBrowseButton(fileChooserDescriptor, project) + .bindText(file) + .validationOnApply(errorHolder.asValidation()) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + } + .bottomGap(if (supportsWrite) BottomGap.NONE else BottomGap.SMALL) + + row { + cell( + BorderLayoutPanel().apply { + addToCenter( + JBLabel( + GeneralBundle.message("file-handling.drop-file-here"), + AllIcons.Actions.Download, + SwingConstants.CENTER, + ) + ) + border = UIUtil.getTextFieldBorder() + dropTarget = DropTarget(this, FileDropHandler(project) { file.set(it.toString()) }) + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + + if (supportsWrite) { + row { + label(GeneralBundle.message("file-handling.write-format.title")).gap(RightGap.SMALL) + segmentedButton(WriteFormat.entries) { text = it.title } + .bind(writeFormat) + .gap(RightGap.SMALL) + contextHelp( + WriteFormat.entries.joinToString(separator = "", prefix = "
      ", postfix = "
    ") { + "
  • ${it.title}: ${it.description}
  • " + } + ) + } + + row { comment("").bindText(lastWriteInformation).resizableColumn() } + } + } + + fun readFromFile(): ByteArray { + assertBackgroundThread() + + val fileToUse = Paths.get(file.get()) + + checkFile(fileToUse) + + if (!Files.exists(fileToUse)) { + throw IllegalStateException(GeneralBundle.message("file-handling.error.file-not-exist")) + } else if (!Files.isReadable(fileToUse)) { + throw IllegalStateException(GeneralBundle.message("file-handling.error.file-is-not-readable")) + } + + return Files.readAllBytes(fileToUse) + } + + fun writeToFile(bytes: ByteArray) { + check(supportsWrite) + assertBackgroundThread() + + val fileToUse = Paths.get(file.get()) + + checkFile(fileToUse) + if (Files.exists(fileToUse) && !Files.isWritable(fileToUse)) { + throw IllegalStateException(GeneralBundle.message("file-handling.error.file-is-not-writable")) + } + + val content = + when (writeFormat.get()) { + WriteFormat.BINARY -> bytes + WriteFormat.HEX -> bytes.toHexString().encodeToByteArray() + } + + try { + Files.write( + fileToUse, + content, + StandardOpenOption.WRITE, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + ) + lastWriteInformation.set( + GeneralBundle.message( + "file-handling.last-write.details", + DateFormatUtil.formatDateTime(System.currentTimeMillis()), + StringUtil.formatFileSize(bytes.size.toLong()), + ) + ) + } catch (e: Exception) { + throw IllegalStateException(GeneralBundle.message("file-handling.error.failed-to-write"), e) + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun checkFile(file: Path?) { + if (file == null || file.toString().isBlank()) { + throw IllegalStateException(GeneralBundle.message("file-handling.error.no-file-selected")) + } else if (Files.isDirectory(file)) { + throw IllegalStateException(GeneralBundle.message("file-handling.error.file-is-directory")) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + enum class WriteFormat(val title: String, val description: String) { + BINARY( + GeneralBundle.message("file-handling.write-format.binary.title"), + GeneralBundle.message("file-handling.write-format.binary.description"), + ), + HEX( + GeneralBundle.message("file-handling.write-format.hex.title"), + GeneralBundle.message("file-handling.write-format.hex.description"), + ), + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/NotBlankInputValidator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/NotBlankInputValidator.kt new file mode 100644 index 00000000..b4684c6e --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/NotBlankInputValidator.kt @@ -0,0 +1,17 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common + +import com.intellij.openapi.ui.InputValidator + +class NotBlankInputValidator : InputValidator { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun checkInput(inputString: String?): Boolean = inputString?.isNotBlank() ?: false + + override fun canClose(inputString: String?): Boolean = inputString?.isNotBlank() ?: false + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/NotificationUtils.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/NotificationUtils.kt similarity index 61% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/NotificationUtils.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/NotificationUtils.kt index 8292d844..d7857135 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/NotificationUtils.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/NotificationUtils.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType @@ -7,20 +7,21 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project object NotificationUtils { - // -- Variables --------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Variables ----------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun notifyOnToolWindow( message: String, project: Project?, notificationType: NotificationType = NotificationType.INFORMATION, - vararg actions: AnAction + vararg actions: AnAction, ) { ApplicationManager.getApplication().invokeLater { - val notification = NotificationGroupManager.getInstance() - .getNotificationGroup("Developer Tools Plugin Notifications") - .createNotification(message, notificationType) + val notification = + NotificationGroupManager.getInstance() + .getNotificationGroup("Developer Tools Plugin Notifications") + .createNotification(message, notificationType) actions.forEach { notification.addAction(it) } @@ -28,6 +29,6 @@ object NotificationUtils { } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/PropertyComponentPredicate.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/PropertyComponentPredicate.kt similarity index 57% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/PropertyComponentPredicate.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/PropertyComponentPredicate.kt index 6b8fa76c..476ae738 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/PropertyComponentPredicate.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/PropertyComponentPredicate.kt @@ -1,6 +1,4 @@ -@file:Suppress("UnstableApiUsage") - -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.openapi.observable.dispatcher.SingleEventDispatcher import com.intellij.openapi.observable.properties.ObservableProperty @@ -8,31 +6,33 @@ import com.intellij.ui.layout.ComponentPredicate class PropertyComponentPredicate( private val property: ObservableProperty, - private val expectedValue: T + private val expectedValue: T, ) : ComponentPredicate() { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val changeDispatcher = SingleEventDispatcher.create() - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { - property.afterChange { value -> - changeDispatcher.fireEvent(value) - } + property.afterChange { value -> changeDispatcher.fireEvent(value) } } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // override fun addListener(listener: (Boolean) -> Unit) { - changeDispatcher.whenEventHappened { value -> - listener(value?.equals(expectedValue) ?: false) - } + changeDispatcher.whenEventHappened { value -> listener(value?.equals(expectedValue) ?: false) } } override fun invoke(): Boolean = property.get()?.equals(expectedValue) ?: false - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + fun ObservableProperty.createPredicate(expectedValue: T): PropertyComponentPredicate = + PropertyComponentPredicate(this, expectedValue) + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ScrollPaneBuilder.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ScrollPaneBuilder.kt similarity index 79% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ScrollPaneBuilder.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ScrollPaneBuilder.kt index 142f3236..305fec36 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ScrollPaneBuilder.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ScrollPaneBuilder.kt @@ -1,21 +1,19 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.ui.components.JBScrollPane import com.intellij.util.ui.JBUI import javax.swing.JComponent import javax.swing.ScrollPaneConstants -class ScrollPaneBuilder( - private val component: JComponent -) { - // -- Properties -------------------------------------------------------------------------------------------------- // +class ScrollPaneBuilder(private val component: JComponent) { + // -- Properties ---------------------------------------------------------- // private var withoutBorder: Boolean = true private var verticalScrollBarPolicy: Int = ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED private var horizontalScrollBarPolicy: Int = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun withoutBorder(withoutBorder: Boolean): ScrollPaneBuilder { this.withoutBorder = withoutBorder @@ -57,7 +55,7 @@ class ScrollPaneBuilder( return scrollPane } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/SimpleTable.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/SimpleTable.kt similarity index 53% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/SimpleTable.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/SimpleTable.kt index 1fa9146e..3d539e3f 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/SimpleTable.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/SimpleTable.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider @@ -12,6 +12,8 @@ import com.intellij.ui.table.JBTable import com.intellij.util.ui.ColumnInfo import com.intellij.util.ui.ListTableModel import com.intellij.util.ui.UIUtil +import dev.turingcomplete.intellijdevelopertoolsplugin.common.CopyValuesAction +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginCommonDataKeys import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo import java.awt.Dimension import javax.swing.ListSelectionModel @@ -22,29 +24,33 @@ class SimpleTable( items: List, columns: List>, private val toCopyValue: (T) -> String, - initialSortedColumn: Pair = Pair(0, SortOrder.ASCENDING) + initialSortedColumn: Pair = Pair(0, SortOrder.ASCENDING), ) : JBTable(), DataProvider { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { - model = ListTableModel(columns.toTypedArray(), items, initialSortedColumn.first, initialSortedColumn.second) + model = + ListTableModel( + columns.toTypedArray(), + items, + initialSortedColumn.first, + initialSortedColumn.second, + ) setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) rowSelectionAllowed = true columnSelectionAllowed = false setContextMenu( - this::class.java.name, DefaultActionGroup( - CopyValuesAction(valueToString = { - @Suppress("UNCHECKED_CAST") - toCopyValue(it as T) - }) - ) + this::class.java.name, + DefaultActionGroup( + CopyValuesAction(valueToString = { @Suppress("UNCHECKED_CAST") toCopyValue(it as T) }) + ), ) setEmptyState("No values") TableSpeedSearch.installOn(this) } - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // fun reload() { model.uncheckedCastTo>().apply { @@ -53,33 +59,33 @@ class SimpleTable( } } - override fun getData(dataId: String): Any? = when { - PluginCommonDataKeys.SELECTED_VALUES.`is`(dataId) -> { - val tableModel = model.uncheckedCastTo>() - selectedRows - .map { tableModel.getRowValue(rowSorter.convertRowIndexToModel(it)) } - .toList() - } + override fun getData(dataId: String): Any? = + when { + PluginCommonDataKeys.SELECTED_VALUES.`is`(dataId) -> { + val tableModel = model.uncheckedCastTo>() + selectedRows.map { tableModel.getRowValue(rowSorter.convertRowIndexToModel(it)) }.toList() + } - else -> null - } - - fun asBalloon(parentDisposable: Disposable) = JBPopupFactory.getInstance() - .createBalloonBuilder(ScrollPaneFactory.createScrollPane(this, false).apply { - preferredSize = Dimension(400, 300) - }) - .setDialogMode(true) - .setFillColor(UIUtil.getPanelBackground()) - .setBorderColor(JBColor.border()) - .setBlockClicksThroughBalloon(true) - .setRequestFocus(true) - .setDisposable(parentDisposable) - .createBalloon() - .apply { - setAnimationEnabled(false) + else -> null } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + fun asBalloon(parentDisposable: Disposable) = + JBPopupFactory.getInstance() + .createBalloonBuilder( + ScrollPaneFactory.createScrollPane(this, false).apply { + preferredSize = Dimension(400, 300) + } + ) + .setDialogMode(true) + .setFillColor(UIUtil.getPanelBackground()) + .setBorderColor(JBColor.border()) + .setBlockClicksThroughBalloon(true) + .setRequestFocus(true) + .setDisposable(parentDisposable) + .createBalloon() + .apply { setAnimationEnabled(false) } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/SimpleToggleAction.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/SimpleToggleAction.kt similarity index 62% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/SimpleToggleAction.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/SimpleToggleAction.kt index a03148c0..8cad3cad 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/SimpleToggleAction.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/SimpleToggleAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent @@ -6,16 +6,16 @@ import com.intellij.openapi.project.DumbAwareToggleAction import javax.swing.Icon class SimpleToggleAction( - text: String, - icon: Icon?, - private val isSelected: () -> Boolean, - private val setSelected: (Boolean) -> Unit, - private val isEnabled: (() -> Boolean)? = null, + text: String, + icon: Icon?, + private val isSelected: () -> Boolean, + private val setSelected: (Boolean) -> Unit, + private val isEnabled: (() -> Boolean)? = null, ) : DumbAwareToggleAction(text, "", icon) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun isSelected(e: AnActionEvent): Boolean = isSelected.invoke() @@ -30,7 +30,7 @@ class SimpleToggleAction( } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPane.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPane.kt new file mode 100644 index 00000000..d1a7e9da --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/TitledTabbedPane.kt @@ -0,0 +1,68 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common + +import com.intellij.ide.ui.laf.darcula.ui.DarculaTabbedPaneUI +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTabbedPane +import com.intellij.util.ui.JBUI +import java.awt.Font +import java.awt.Graphics +import javax.swing.JComponent +import javax.swing.JPanel + +class TitledTabbedPane(title: String, tabs: List>) : JBTabbedPane() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + + init { + tabComponentInsets = JBUI.insetsTop(8) + + addTab("", JPanel()) + setTabComponentAt(0, JBLabel(title).apply { font = font?.deriveFont(Font.BOLD) }) + setEnabledAt(0, false) + + tabs.forEach { addTab(it.first, it.second) } + + selectedIndex = 1 + setUI(TitleTabAwareTabbedPaneUi()) + } + + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + private class TitleTabAwareTabbedPaneUi : DarculaTabbedPaneUI() { + + override fun paintTabBackground( + g: Graphics, + tabPlacement: Int, + tabIndex: Int, + x: Int, + y: Int, + w: Int, + h: Int, + isSelected: Boolean, + ) { + if (tabIndex >= 1) { + super.paintTabBackground(g, tabPlacement, tabIndex, x, y, w, h, isSelected) + } else { + // Remove hover on the title tab + g.color = tabPane.getBackground() + + // The following logic was copied from the super method + var updatedW = w + var updatedH = h + if (tabPane.tabLayoutPolicy == SCROLL_TAB_LAYOUT) { + if (tabPlacement == LEFT || tabPlacement == RIGHT) { + updatedW -= offset + } else { + updatedH -= offset + } + } + + g.fillRect(x, y, updatedW, updatedH) + } + } + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiExtensions.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiExtensions.kt new file mode 100644 index 00000000..42594f46 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiExtensions.kt @@ -0,0 +1,310 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common + +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.HyperlinkLabel +import com.intellij.ui.JBColor +import com.intellij.ui.UIBundle +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBRadioButton +import com.intellij.ui.components.JBTabbedPane +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import com.intellij.ui.tabs.TabInfo +import com.intellij.util.ui.JBEmptyBorder +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.components.BorderLayoutPanel +import java.awt.Color +import java.awt.Font +import java.awt.event.InputEvent +import java.awt.event.ItemEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import java.math.BigDecimal +import java.math.MathContext +import javax.swing.JComponent +import javax.swing.JLabel +import javax.swing.JTable +import javax.swing.JTextField +import javax.swing.ToolTipManager +import javax.swing.border.CompoundBorder + +// -- Properties ---------------------------------------------------------- // +// -- Exported Methods ---------------------------------------------------- // + +/** The UI DSL only verifies the range of an `intTextField` on a user input. */ +@Suppress("UnstableApiUsage") +fun Cell.validateLongValue(range: LongRange? = null) = + this.apply { + validationInfo { + if (this@validateLongValue.component.isEnabled) { + val value = this@validateLongValue.component.text.toLongOrNull() + when { + value == null -> error(UIBundle.message("please.enter.a.number")) + range != null && value !in range -> + error(UIBundle.message("please.enter.a.number.from.0.to.1", range.first, range.last)) + else -> null + } + } else { + null + } + } + } + +@Suppress("UnstableApiUsage") +fun Cell.validateBigDecimalValue( + minInclusive: BigDecimal? = null, + mathContext: MathContext = MathContext.UNLIMITED, + toBigDecimal: (String) -> BigDecimal? = { it.toBigDecimalOrNull(mathContext) }, +) = + this.apply { + validationInfo { + if (!this@validateBigDecimalValue.component.isEnabled) { + return@validationInfo null + } + val value: BigDecimal? = + try { + toBigDecimal(this@validateBigDecimalValue.component.text) + } catch (_: Exception) { + return@validationInfo error("Please enter a valid number") + } + when { + value == null -> error("Please enter a number") + minInclusive != null && value < minInclusive -> + error("Enter a number greater than or equal to 0") + else -> null + } + } + } + +fun Cell.bind(property: ObservableMutableProperty, value: T) = + this.apply { + this.applyToComponent { + isSelected = property.get() == value + + this.addItemListener { event -> + if (event.stateChange == ItemEvent.SELECTED) { + property.set(value) + } + } + } + } + +/** + * IntelliJ's `bindIntText` will silently fail if the input is empty and will not execute any other + * validators. + */ +@Suppress("UnstableApiUsage") +fun Cell.bindIntTextImproved(property: ObservableMutableProperty) = + this.apply { + applyToComponent { + text = property.get().toString() + property.afterChange { text = it.toString() } + } + this.whenTextChangedFromUi { it.toIntOrNull()?.let { intValue -> property.set(intValue) } } + } + +/** + * IntelliJ's `bindIntText` will silently fail if the input is empty and will not execute any other + * validators. + */ +@Suppress("UnstableApiUsage") +fun Cell.bindLongTextImproved(property: ObservableMutableProperty) = + this.apply { + applyToComponent { + text = property.get().toString() + property.afterChange { text = it.toString() } + } + this.whenTextChangedFromUi { it.toLongOrNull()?.let { longValue -> property.set(longValue) } } + } + +/** + * IntelliJ's `bindIntText` will silently fail if the input is empty and will not execute any other + * validators. + */ +@Suppress("UnstableApiUsage") +fun Cell.bindDoubleTextImproved(property: ObservableMutableProperty) = + this.apply { + applyToComponent { + text = property.get().toString() + property.afterChange { text = it.toString() } + } + this.whenTextChangedFromUi { + it.toDoubleOrNull()?.let { doubleValue -> property.set(doubleValue) } + } + } + +@Suppress("UnstableApiUsage") +fun Cell.validateNonEmpty(errorMessage: String) = + this.applyToComponent { + validationInfo { + if (it.text.isEmpty()) { + ValidationInfo(errorMessage) + } else { + null + } + } + } + +fun Cell.setValidationResultBorder() = + this.applyToComponent { + border = CompoundBorder(ValidationResultBorder(this), JBEmptyBorder(3, 5, 3, 5)) + } + +@Suppress("UnstableApiUsage") +fun Cell.validateMinMaxValueRelation( + side: ValidateMinIntValueSide, + getOppositeValue: () -> Int, +): Cell = + this.validationInfo { + if (this.component.isEnabled) { + it.text?.toIntOrNull()?.let { thisValue -> + when { + side == ValidateMinIntValueSide.MIN && thisValue > getOppositeValue() -> { + ValidationInfo("Minimum must be smaller than or equal to maximum") + } + + side == ValidateMinIntValueSide.MAX && thisValue < getOppositeValue() -> + ValidationInfo("Maximum must be larger than or equal to minimum") + + else -> null + } + } + } else { + null + } + } + +fun JBLabel.copyable() = this.apply { setCopyable(true) } + +fun JComponent.wrapWithToolBar( + actionEventPlace: String, + actions: ActionGroup, + toolBarPlace: ToolBarPlace, +): JComponent { + return BorderLayoutPanel().apply { + val actionToolbar = + ActionManager.getInstance() + .createActionToolbar(actionEventPlace, actions, toolBarPlace.horizontal) + actionToolbar.targetComponent = this@wrapWithToolBar + + when (toolBarPlace) { + ToolBarPlace.LEFT -> { + addToLeft(actionToolbar.component.withNoLeftBorderInset()) + addToCenter(this@wrapWithToolBar) + } + + ToolBarPlace.RIGHT, + ToolBarPlace.APPEND -> { + addToCenter(this@wrapWithToolBar) + addToRight(actionToolbar.component.withNoRightBorderInset()) + } + } + } +} + +fun JComponent.withNoRightBorderInset(): JComponent { + val insets = border?.getBorderInsets(this) ?: JBUI.emptyInsets() + border = JBUI.Borders.empty(insets.top, insets.left, insets.bottom, 0) + return this +} + +fun JComponent.withNoLeftBorderInset(): JComponent { + val insets = border?.getBorderInsets(this) ?: JBUI.emptyInsets() + border = JBUI.Borders.empty(insets.top, 0, insets.bottom, insets.right) + return this +} + +fun JTable.setContextMenu(place: String, actionGroup: ActionGroup) { + val mouseAdapter = + object : MouseAdapter() { + + override fun mousePressed(e: MouseEvent) { + handleMouseEvent(e) + } + + override fun mouseReleased(e: MouseEvent) { + handleMouseEvent(e) + } + + private fun handleMouseEvent(e: InputEvent) { + if (e is MouseEvent && e.isPopupTrigger) { + ActionManager.getInstance() + .createActionPopupMenu(place, actionGroup) + .component + .show(e.component, e.x, e.y) + } + } + } + addMouseListener(mouseAdapter) +} + +fun Cell.changeFont(scale: Float = 1.0f, style: Int = Font.PLAIN) = + this.applyToComponent { font = JBFont.create(this.font.deriveFont(style), false).biggerOn(scale) } + +fun Cell.monospaceFont(scale: Float = 1.0f, style: Int = Font.PLAIN) = + this.applyToComponent { + font = JBFont.create(Font(Font.MONOSPACED, style, this.font.size)).biggerOn(scale) + } + +fun EditorEx.setLanguage(language: Language) { + val syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(language, project, null) + highlighter = + EditorHighlighterFactory.getInstance() + .createEditorHighlighter(syntaxHighlighter, EditorColorsManager.getInstance().globalScheme) +} + +fun Row.hyperLink(title: String, url: String) { + cell(HyperlinkLabel(title).apply { setHyperlinkTarget(url) }) +} + +fun Color.toJBColor() = this as? JBColor ?: JBColor(this, this) + +@Suppress("UNCHECKED_CAST") fun TabInfo.castedObject(): T = this.`object` as T + +fun Cell.registerDynamicToolTip(toolTipText: () -> String?) { + this.component.toolTipText = null + ToolTipManager.sharedInstance().registerComponent(this.component) + + this.component.addMouseListener( + object : MouseAdapter() { + override fun mouseEntered(e: MouseEvent?) { + this@registerDynamicToolTip.component.toolTipText = toolTipText() + } + } + ) +} + +fun JBTabbedPane.onSelectionChanged(onSelectionChanged: (JComponent) -> Unit): JBTabbedPane { + addChangeListener { onSelectionChanged(selectedComponent as JComponent) } + + return this +} + +// -- Private Methods ---------------------------------------------------- // +// -- Inner Type ---------------------------------------------------------- // + +enum class ValidateMinIntValueSide { + MIN, + MAX, +} + +// -- Inner Type ---------------------------------------------------------- // + +enum class ToolBarPlace(val horizontal: Boolean) { + + LEFT(false), + RIGHT(false), + APPEND(true), +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiUtils.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiUtils.kt new file mode 100644 index 00000000..948bad07 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/UiUtils.kt @@ -0,0 +1,168 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common + +import com.intellij.diff.DiffContentFactory +import com.intellij.diff.DiffManager +import com.intellij.diff.requests.DiffRequest +import com.intellij.diff.requests.SimpleDiffRequest +import com.intellij.openapi.actionSystem.ActionGroup +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.ToggleAction +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.util.PopupUtil +import com.intellij.ui.HyperlinkLabel +import com.intellij.ui.JBColor +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.popup.PopupState +import com.intellij.util.ui.ColumnInfo +import com.intellij.util.ui.UIUtil +import java.awt.Dimension +import java.awt.event.InputEvent +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent +import javax.swing.Icon +import javax.swing.JComponent + +object UiUtils { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + fun showDiffDialog( + title: String, + firstTitle: String, + secondTitle: String, + firstText: String, + secondText: String, + ) { + val diffContentFactory = DiffContentFactory.getInstance() + val firstContent = diffContentFactory.create(firstText) + val secondContent = diffContentFactory.create(secondText) + val request: DiffRequest = + SimpleDiffRequest(title, firstContent, secondContent, firstTitle, secondTitle) + DiffManager.getInstance().showDiff(null, request) + } + + fun dumbAwareAction( + title: String, + icon: Icon? = null, + isEnabledAndVisible: () -> Boolean = { true }, + action: (AnActionEvent) -> Unit, + ) = + object : DumbAwareAction(title, null, icon) { + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = isEnabledAndVisible() + } + + override fun actionPerformed(e: AnActionEvent) { + action(e) + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + } + + @Suppress("UnstableApiUsage") + fun actionsPopup(title: String, icon: Icon? = null, actions: List): ActionGroup = + object : ActionGroup(title, null, icon), DumbAware { + + private val popupState = PopupState.forPopup() + + init { + isPopup = true + templatePresentation.isPerformGroup = actions.isNotEmpty() + } + + override fun getChildren(e: AnActionEvent?): Array = actions.toTypedArray() + + override fun actionPerformed(e: AnActionEvent) { + if (popupState.isRecentlyHidden) { + return + } + + val popup = + JBPopupFactory.getInstance() + .createActionGroupPopup( + null, + this, + e.dataContext, + JBPopupFactory.ActionSelectionAid.MNEMONICS, + true, + ) + popupState.prepareToShow(popup) + PopupUtil.showForActionButtonEvent(popup, e) + } + } + + fun createLink(title: String, url: String): HyperlinkLabel { + return HyperlinkLabel(title).apply { setHyperlinkTarget(url) } + } + + fun createToggleAction(title: String, isSelected: () -> Boolean, setSelected: (Boolean) -> Unit) = + object : ToggleAction(title) { + override fun isSelected(e: AnActionEvent): Boolean = isSelected() + + override fun setSelected(e: AnActionEvent, state: Boolean) { + setSelected(state) + } + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT + } + + fun createContextMenuMouseListener(place: String, actionGroup: (MouseEvent) -> ActionGroup?) = + object : MouseAdapter() { + override fun mousePressed(e: MouseEvent) { + handleMouseEvent(e) + } + + override fun mouseReleased(e: MouseEvent) { + handleMouseEvent(e) + } + + private fun handleMouseEvent(e: InputEvent) { + if (e is MouseEvent && e.isPopupTrigger) { + actionGroup(e)?.let { + ActionManager.getInstance() + .createActionPopupMenu(place, it) + .component + .show(e.component, e.x, e.y) + } + } + } + } + + fun simpleColumnInfo( + name: String, + displayValue: (T) -> String, + sortValue: (T) -> Comparable<*>, + ) = + object : ColumnInfo(name) { + + override fun valueOf(item: T): String = displayValue(item) + + override fun getComparator(): Comparator = compareBy { sortValue(it) } + } + + fun createPopup(content: JComponent, width: Int = 600, height: Int = 450): Balloon = + JBPopupFactory.getInstance() + .createBalloonBuilder( + ScrollPaneFactory.createScrollPane(content, true).apply { + preferredSize = Dimension(width, height) + } + ) + .setDialogMode(true) + .setFillColor(UIUtil.getPanelBackground()) + .setBorderColor(JBColor.border()) + .setBlockClicksThroughBalloon(true) + .setRequestFocus(true) + .createBalloon() + .apply { setAnimationEnabled(false) } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ValidationResultBorder.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ValidationResultBorder.kt similarity index 59% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ValidationResultBorder.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ValidationResultBorder.kt index 8626fdd1..f2ba3501 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ValidationResultBorder.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/ValidationResultBorder.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common import com.intellij.util.ObjectUtils import com.intellij.util.ui.JBUI @@ -7,36 +7,41 @@ import java.awt.Graphics import javax.swing.JComponent import javax.swing.border.LineBorder -internal class ValidationResultBorder( +class ValidationResultBorder( private val ownerComponent: JComponent, - private val focusComponent: JComponent = ownerComponent + private val focusComponent: JComponent = ownerComponent, ) : LineBorder(defaultBorderColor, 1) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val errorBorder by lazy { JBUI.CurrentTheme.Focus.errorColor(false) } private val errorFocusBorder by lazy { JBUI.CurrentTheme.Focus.errorColor(true) } private val warningBorder by lazy { JBUI.CurrentTheme.Focus.warningColor(false) } private val warningFocusBorder by lazy { JBUI.CurrentTheme.Focus.warningColor(true) } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun paintBorder(c: Component?, g: Graphics?, x: Int, y: Int, width: Int, height: Int) { - val outline = ObjectUtils.tryCast(ownerComponent.getClientProperty("JComponent.outline"), String::class.java) - this.lineColor = when (outline) { - "error" -> if (focusComponent.hasFocus()) errorFocusBorder else errorBorder - "warning" -> if (focusComponent.hasFocus()) warningFocusBorder else warningBorder - else -> defaultBorderColor - } + val outline = + ObjectUtils.tryCast( + ownerComponent.getClientProperty("JComponent.outline"), + String::class.java, + ) + this.lineColor = + when (outline) { + "error" -> if (focusComponent.hasFocus()) errorFocusBorder else errorBorder + "warning" -> if (focusComponent.hasFocus()) warningFocusBorder else warningBorder + else -> defaultBorderColor + } super.paintBorder(c, g, x, y, width, height) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // private companion object { private val defaultBorderColor = JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground() } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/RegexOption.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/RegexOption.kt similarity index 72% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/RegexOption.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/RegexOption.kt index aaa1d328..a68fc443 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/RegexOption.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/RegexOption.kt @@ -1,58 +1,58 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.regex +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex import java.util.regex.Pattern enum class RegexOption(val patternFlag: Int, val title: String, val description: String? = null) { - // -- Values ------------------------------------------------------------------------------------------------------ // + // -- Values -------------------------------------------------------------- // CASE_INSENSITIVE( Pattern.CASE_INSENSITIVE, "Case-insensitive", - "Case-insensitive matching will use characters for the US-ASCII charset for matching." + "Case-insensitive matching will use characters for the US-ASCII charset for matching.", ), UNICODE_CASE( Pattern.UNICODE_CASE, "Unicode-aware", - "The case insensitive option will use the Unicode standard." + "The case insensitive option will use the Unicode standard.", ), MULTILINE( Pattern.MULTILINE, "Multiline", - "The expressions ^ and \$ match just after or just before, respectively, a line terminator or the end of the input sequence." + "The expressions ^ and \$ match just after or just before, respectively, a line terminator or the end of the input sequence.", ), DOTALL( Pattern.DOTALL, "Dotall", - "The expression . will also match line terminators." + "The expression . will also match line terminators.", ), CANON_EQ( Pattern.CANON_EQ, "Canonical equivalence", - "Two characters will be considered to match if, and only if, their full canonical decompositions match." + "Two characters will be considered to match if, and only if, their full canonical decompositions match.", ), UNIX_LINES( Pattern.UNIX_LINES, "Unix line endings", - "Only the \\n line terminator is recognized in the behavior of ., ^, and \$." + "Only the \\n line terminator is recognized in the behavior of ., ^, and \$.", ), LITERAL( Pattern.LITERAL, "Literal parsing of the pattern", - "The input string that specifies the pattern will be treated as a sequence of literal characters." + "The input string that specifies the pattern will be treated as a sequence of literal characters.", ), COMMENTS( Pattern.COMMENTS, "Permit whitespace and comments in pattern", - "Whitespace will be ignored, and embedded comments starting with # are ignored until the end of a line." + "Whitespace will be ignored, and embedded comments starting with # are ignored until the end of a line.", ); - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun isSelected(regexOptionFlag: Int) = regexOptionFlag.and(patternFlag) != 0 - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/RegexTextField.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/RegexTextField.kt similarity index 69% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/RegexTextField.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/RegexTextField.kt index b49d4dd2..9a2c4ab9 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/RegexTextField.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/RegexTextField.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.regex +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex import com.intellij.openapi.Disposable import com.intellij.openapi.editor.Editor @@ -7,10 +7,10 @@ import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.project.Project import com.intellij.ui.LanguageTextField import com.intellij.util.ui.JBFont -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import java.lang.Boolean.TRUE import org.intellij.lang.regexp.RegExpLanguage import org.intellij.lang.regexp.intention.CheckRegExpForm -import java.lang.Boolean.TRUE class RegexTextField( project: Project?, @@ -18,23 +18,25 @@ class RegexTextField( textProperty: ValueProperty, ) : LanguageTextField(RegExpLanguage.INSTANCE, project, textProperty.get(), true) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private var onTextChangeFromUi = mutableListOf<((String) -> Unit)>() - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // init { font = JBFont.create(font, false).biggerOn(1.4f) - addDocumentListener(object : DocumentListener { - override fun documentChanged(event: DocumentEvent) { - val text = event.document.text - textProperty.set(text, TEXT_PROPERTY_CHANGE_ID) - onTextChangeFromUi.forEach { it(text) } + addDocumentListener( + object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + val text = event.document.text + textProperty.set(text, TEXT_PROPERTY_CHANGE_ID) + onTextChangeFromUi.forEach { it(text) } + } } - }) + ) textProperty.afterChangeConsumeEvent(parentDisposable) { event -> if (event.newValue != event.oldValue && event.id != TEXT_PROPERTY_CHANGE_ID) { @@ -52,12 +54,12 @@ class RegexTextField( editor.putUserData(CheckRegExpForm.Keys.CHECK_REG_EXP_EDITOR, TRUE) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { private const val TEXT_PROPERTY_CHANGE_ID = "RegexTextField" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/SelectRegexOptionsAction.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/SelectRegexOptionsAction.kt similarity index 56% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/SelectRegexOptionsAction.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/SelectRegexOptionsAction.kt index 44f6d2ad..1da62497 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/regex/SelectRegexOptionsAction.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/common/regex/SelectRegexOptionsAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.regex +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.ActionToolbar.DEFAULT_MINIMUM_BUTTON_SIZE @@ -17,81 +17,88 @@ import com.intellij.ui.components.JBCheckBox import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.whenStateChangedFromUi import com.intellij.util.ui.UIUtil -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other.RegularExpressionMatcher import javax.swing.JComponent class SelectRegexOptionsAction( private val parentComponent: () -> JComponent, - private val selectedRegexOptionFlag: ObservableMutableProperty + private val selectedRegexOptionFlag: ObservableMutableProperty, ) : DumbAwareAction("Regular Expression Options", null, AllIcons.General.GearPlain) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private var currentDialog: Balloon? = null - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun actionPerformed(e: AnActionEvent) { if (currentDialog != null) { return } - currentDialog = JBPopupFactory.getInstance().createBalloonBuilder(createRegexOptionPanel()) - .setDialogMode(true) - .setFillColor(UIUtil.getPanelBackground()) - .setBorderColor(JBColor.border()) - .setBlockClicksThroughBalloon(true) - .setRequestFocus(true) - .createBalloon() - .apply { - setAnimationEnabled(false) - addListener(object : JBPopupListener { - override fun onClosed(event: LightweightWindowEvent) { - currentDialog = null - } - }) - show(RelativePoint.getSouthOf(parentComponent()), Balloon.Position.below) - } + currentDialog = + JBPopupFactory.getInstance() + .createBalloonBuilder(createRegexOptionPanel()) + .setDialogMode(true) + .setFillColor(UIUtil.getPanelBackground()) + .setBorderColor(JBColor.border()) + .setBlockClicksThroughBalloon(true) + .setRequestFocus(true) + .createBalloon() + .apply { + setAnimationEnabled(false) + addListener( + object : JBPopupListener { + override fun onClosed(event: LightweightWindowEvent) { + currentDialog = null + } + } + ) + show(RelativePoint.getSouthOf(parentComponent()), Balloon.Position.below) + } } override fun getActionUpdateThread() = ActionUpdateThread.EDT - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // @Suppress("UnstableApiUsage") private fun createRegexOptionPanel() = panel { val regexOptionCheckBox = mutableMapOf() val setSelectedRegexOptionFlag: () -> Unit = { - selectedRegexOptionFlag.set(regexOptionCheckBox.filter { it.value.isSelected }.map { it.key.patternFlag }.sum()) + selectedRegexOptionFlag.set( + regexOptionCheckBox.filter { it.value.isSelected }.map { it.key.patternFlag }.sum() + ) } val selectedRegexOptionFlag = selectedRegexOptionFlag.get() RegexOption.entries.forEach { regexOption -> row { - regexOptionCheckBox[regexOption] = checkBox(regexOption.title) - .comment(regexOption.description) - .applyToComponent { isSelected = regexOption.isSelected(selectedRegexOptionFlag) } - .whenStateChangedFromUi { setSelectedRegexOptionFlag() } - .component + regexOptionCheckBox[regexOption] = + checkBox(regexOption.title) + .comment(regexOption.description) + .applyToComponent { isSelected = regexOption.isSelected(selectedRegexOptionFlag) } + .whenStateChangedFromUi { setSelectedRegexOptionFlag() } + .component } } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { fun createActionButton(selectedRegexOptionFlag: ObservableMutableProperty): ActionButton { lateinit var actionButton: ActionButton - actionButton = ActionButton( - SelectRegexOptionsAction({ actionButton }, selectedRegexOptionFlag), - null, - RegularExpressionMatcher::class.java.name, - DEFAULT_MINIMUM_BUTTON_SIZE - ) + actionButton = + ActionButton( + SelectRegexOptionsAction({ actionButton }, selectedRegexOptionFlag), + null, + SelectRegexOptionsAction::class.java.name, + DEFAULT_MINIMUM_BUTTON_SIZE, + ) return actionButton } } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CliCommandConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CliCommandConverter.kt new file mode 100644 index 00000000..1df3775f --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CliCommandConverter.kt @@ -0,0 +1,144 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindText +import com.intellij.util.system.OS +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.BidirectionalConverter +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.ConversionSideHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import java.lang.System.lineSeparator + +class CliCommandConverter( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + BidirectionalConverter( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("cli-command-converter.title"), + sourceTitle = UiToolsBundle.message("cli-command-converter.source-title"), + targetTitle = UiToolsBundle.message("cli-command-converter.target-title"), + toSourceTitle = UiToolsBundle.message("cli-command-converter.to-source-title"), + toTargetTitle = UiToolsBundle.message("cli-command-converter.to-target-title"), + ) { + // -- Properties ---------------------------------------------------------- // + + private val lineBreakDelimiter = + configuration.register("lineBreakDelimiter", defaultLineBreakDelimiter) + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun Panel.buildTargetTopConfigurationUi() { + row { + textField() + .label(UiToolsBundle.message("cli-command-converter.line-break-delimiter")) + .bindText(lineBreakDelimiter) + } + } + + override fun ConversionSideHandler.addSourceTextInputOutputHandler() { + addTextInputOutputHandler( + id = defaultSourceInputOutputHandlerId, + exampleText = EXAMPLE_SOURCE_TEXT, + ) + } + + override fun ConversionSideHandler.addTargetTextInputOutputHandler() { + addTextInputOutputHandler( + id = defaultTargetInputOutputHandlerId, + exampleText = String(doConvertToTarget(EXAMPLE_SOURCE_TEXT.toByteArray())), + ) + } + + override fun doConvertToTarget(source: ByteArray): ByteArray { + val lines = mutableListOf() + + var currentLine = "" + cliCommandAllArgumentsSplitRegex + .findAll(String(source)) + .mapNotNull { matchResult -> + matchResult.groups[1]?.let { "\"${it.value}\"" } + ?: matchResult.groups[2]?.let { "'${it.value}'" } + ?: matchResult.groups[0]?.value + } + .map { it.trim() } + .forEach { token -> + currentLine = + if (token.startsWith("-")) { + lines.add(currentLine) + + if (lines.isNotEmpty()) { + "$CLI_COMMAND_INDENT$token" + } else { + token + } + } else if (currentLine.isBlank()) { + token + } else { + "$currentLine $token" + } + } + lines.add(currentLine) + + return lines + .filter { it.isNotBlank() } + .joinToString(" ${lineBreakDelimiter}${lineSeparator()}") + .toByteArray() + } + + override fun doConvertToSource(target: ByteArray): ByteArray = + cliCommandAllArgumentsSplitRegex + .findAll(String(target)) + .mapNotNull { matchResult -> + matchResult.groups[1]?.let { "\"${it.value}\"" } + ?: matchResult.groups[2]?.let { "'${it.value}'" } + ?: matchResult.groups[0]?.value + } + .map { it.trim() } + .filter { it != lineBreakDelimiter.get() } + .joinToString(" ") + .toByteArray() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("cli-command-converter.title"), + contentTitle = UiToolsBundle.message("cli-command-converter.content-title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> CliCommandConverter) = { configuration -> + CliCommandConverter(configuration, parentDisposable, context, project) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val CLI_COMMAND_INDENT = " " + private val defaultLineBreakDelimiter = if (OS.CURRENT == OS.Windows) "^" else "\\" + private val cliCommandAllArgumentsSplitRegex = Regex("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'") + + private const val EXAMPLE_SOURCE_TEXT = + "python script.py --input-file=input.txt --output-file=output.txt --verbose" + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CommonEncodersDecoders.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CommonEncodersDecoders.kt new file mode 100644 index 00000000..f41adb86 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CommonEncodersDecoders.kt @@ -0,0 +1,261 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.common.decodeFromAscii +import dev.turingcomplete.intellijdevelopertoolsplugin.common.encodeToAscii +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.util.Base64 +import org.apache.commons.codec.binary.Base32 + +// -- Properties ---------------------------------------------------------- // +// -- Exported Methods ---------------------------------------------------- // +// -- Private Methods ---------------------------------------------------- // +// -- Inner Type ---------------------------------------------------------- // + +class Base32EncoderDecoder( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EncoderDecoder( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("base32-encoding.title"), + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = Base32().encode(source) + + override fun doConvertToSource(target: ByteArray): ByteArray = Base32().decode(target) + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("base32-encoding.menu-title"), + groupedMenuTitle = UiToolsBundle.message("base32-encoding.grouped-menu-title"), + contentTitle = UiToolsBundle.message("base32-encoding.title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> Base32EncoderDecoder) = { configuration -> + Base32EncoderDecoder(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class Base64EncoderDecoder( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EncoderDecoder( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("base64-encoding.title"), + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = Base64.getEncoder().encode(source) + + override fun doConvertToSource(target: ByteArray): ByteArray = Base64.getDecoder().decode(target) + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("base64-encoding.menu-title"), + groupedMenuTitle = UiToolsBundle.message("base64-encoding.grouped-menu-title"), + contentTitle = UiToolsBundle.message("base64-encoding.title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> Base64EncoderDecoder) = { configuration -> + Base64EncoderDecoder(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class UrlBase64EncoderDecoder( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EncoderDecoder( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("url-base64-encoding.title"), + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = + Base64.getUrlEncoder().encode(source) + + override fun doConvertToSource(target: ByteArray): ByteArray = + Base64.getUrlDecoder().decode(target) + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("url-base64-encoding.menu-title"), + groupedMenuTitle = UiToolsBundle.message("url-base64-encoding.grouped-menu-title"), + contentTitle = UiToolsBundle.message("url-base64-encoding.title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> UrlBase64EncoderDecoder) = { configuration -> + UrlBase64EncoderDecoder(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class MimeBase64EncoderDecoder( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EncoderDecoder( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("mime-base64-encoding.title"), + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = + Base64.getMimeEncoder().encode(source) + + override fun doConvertToSource(target: ByteArray): ByteArray = + Base64.getMimeDecoder().decode(target) + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("mime-base64-encoding.menu-title"), + groupedMenuTitle = UiToolsBundle.message("mime-base64-encoding.grouped-menu-title"), + contentTitle = UiToolsBundle.message("mime-base64-encoding.title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> MimeBase64EncoderDecoder) = { configuration -> + MimeBase64EncoderDecoder(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class AsciiEncoderDecoder( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EncoderDecoder( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("ascii-encoding.title"), + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = source.encodeToAscii() + + override fun doConvertToSource(target: ByteArray): ByteArray = target.decodeFromAscii() + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("ascii-encoding.menu-title"), + groupedMenuTitle = UiToolsBundle.message("ascii-encoding.grouped-menu-title"), + contentTitle = UiToolsBundle.message("ascii-encoding.title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> AsciiEncoderDecoder) = { configuration -> + AsciiEncoderDecoder(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class UrlEncodingEncoderDecoder( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EncoderDecoder( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("url-encoding.title"), + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = + URLEncoder.encode(String(source), StandardCharsets.UTF_8.name()).toByteArray() + + override fun doConvertToSource(target: ByteArray): ByteArray = + URLDecoder.decode(String(target), StandardCharsets.UTF_8.name()).toByteArray() + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("url-encoding.menu-title"), + groupedMenuTitle = UiToolsBundle.message("url-encoding.grouped-menu-title"), + contentTitle = UiToolsBundle.message("url-encoding.title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> UrlEncodingEncoderDecoder) = { configuration -> + UrlEncodingEncoderDecoder(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CommonEscapersUnescapers.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CommonEscapersUnescapers.kt new file mode 100644 index 00000000..e5afa35a --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/CommonEscapersUnescapers.kt @@ -0,0 +1,240 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import org.apache.commons.text.StringEscapeUtils + +// -- Properties ---------------------------------------------------------- // +// -- Exported Methods ---------------------------------------------------- // +// -- Private Methods ---------------------------------------------------- // +// -- Inner Type ---------------------------------------------------------- // + +class HtmlEntitiesEscaperUnescaper( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EscaperUnescaper( + title = UiToolsBundle.message("html-entities-escaper-unescaper.title"), + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = + StringEscapeUtils.escapeHtml4(String(source)).toByteArray() + + override fun doConvertToSource(target: ByteArray): ByteArray = + StringEscapeUtils.unescapeHtml4(String(target)).toByteArray() + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("html-entities-escaper-unescaper.title"), + groupedMenuTitle = + UiToolsBundle.message("html-entities-escaper-unescaper.grouped-menu-title"), + contentTitle = UiToolsBundle.message("html-entities-escaper-unescaper.content-title"), + description = + DeveloperUiToolPresentation.contextHelp( + UiToolsBundle.message("html-entities-escaper-unescaper.description.context-help") + ), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> HtmlEntitiesEscaperUnescaper) = { configuration -> + HtmlEntitiesEscaperUnescaper(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class JavaStringEscaperUnescaper( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EscaperUnescaper( + title = UiToolsBundle.message("java-string-escaper-unescaper.title"), + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = + StringEscapeUtils.escapeJava(String(source)).toByteArray() + + override fun doConvertToSource(target: ByteArray): ByteArray = + StringEscapeUtils.unescapeJava(String(target)).toByteArray() + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("java-string-escaper-unescaper.title"), + groupedMenuTitle = + UiToolsBundle.message("java-string-escaper-unescaper.grouped-menu-title"), + contentTitle = UiToolsBundle.message("java-string-escaper-unescaper.content-title"), + description = + DeveloperUiToolPresentation.contextHelp( + UiToolsBundle.message("java-string-escaper-unescaper.description.context-help") + ), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> JavaStringEscaperUnescaper) = { configuration -> + JavaStringEscaperUnescaper(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class JsonTextEscaperUnescaper( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EscaperUnescaper( + title = UiToolsBundle.message("json-text-escaper-unescaper.title"), + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = + StringEscapeUtils.escapeJson(String(source)).toByteArray() + + override fun doConvertToSource(target: ByteArray): ByteArray = + StringEscapeUtils.unescapeJson(String(target)).toByteArray() + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("json-text-escaper-unescaper.title"), + groupedMenuTitle = UiToolsBundle.message("json-text-escaper-unescaper.grouped-menu-title"), + contentTitle = UiToolsBundle.message("json-text-escaper-unescaper.content-title"), + description = + DeveloperUiToolPresentation.contextHelp( + UiToolsBundle.message("json-text-escaper-unescaper.description.context-help") + ), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> JsonTextEscaperUnescaper) = { configuration -> + JsonTextEscaperUnescaper(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class CsvTextEscaperUnescaper( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EscaperUnescaper( + title = UiToolsBundle.message("csv-text-escaper-unescaper.title"), + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = + StringEscapeUtils.escapeCsv(String(source)).toByteArray() + + override fun doConvertToSource(target: ByteArray): ByteArray = + StringEscapeUtils.unescapeCsv(String(target)).toByteArray() + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("csv-text-escaper-unescaper.title"), + groupedMenuTitle = UiToolsBundle.message("csv-text-escaper-unescaper.grouped-menu-title"), + contentTitle = UiToolsBundle.message("csv-text-escaper-unescaper.content-title"), + description = + DeveloperUiToolPresentation.contextHelp( + UiToolsBundle.message("csv-text-escaper-unescaper.description.context-help") + ), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> CsvTextEscaperUnescaper) = { configuration -> + CsvTextEscaperUnescaper(configuration, parentDisposable, context, project) + } + } +} + +// -- Inner Type ---------------------------------------------------------- // + +class XmlTextEscaperUnescaper( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EscaperUnescaper( + title = UiToolsBundle.message("xml-text-escaper-unescaper.title"), + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + ) { + + override fun doConvertToTarget(source: ByteArray): ByteArray = + StringEscapeUtils.escapeXml11(String(source)).toByteArray() + + override fun doConvertToSource(target: ByteArray): ByteArray = + StringEscapeUtils.unescapeXml(String(target)).toByteArray() + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("xml-text-escaper-unescaper.title"), + groupedMenuTitle = UiToolsBundle.message("xml-text-escaper-unescaper.grouped-menu-title"), + contentTitle = UiToolsBundle.message("xml-text-escaper-unescaper.content-title"), + description = + DeveloperUiToolPresentation.contextHelp( + UiToolsBundle.message("xml-text-escaper-unescaper.description.context-help") + ), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> XmlTextEscaperUnescaper) = { configuration -> + XmlTextEscaperUnescaper(configuration, parentDisposable, context, project) + } + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/DatetimeConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/DatetimeConverter.kt new file mode 100644 index 00000000..bd932109 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/DatetimeConverter.kt @@ -0,0 +1,732 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter + +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.observable.properties.ObservableProperty +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.getUserData +import com.intellij.openapi.ui.putUserData +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.text.StringUtil.stripHtml +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.COLUMNS_MEDIUM +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.actionButton +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.text +import com.intellij.ui.dsl.builder.whenItemSelectedFromUi +import com.intellij.ui.dsl.builder.whenStateChangedFromUi +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import com.intellij.ui.layout.ComboBoxPredicate +import com.intellij.util.Alarm +import com.intellij.util.text.OrdinalFormat.formatEnglish +import dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer +import dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer.Companion.ALL_AVAILABLE_LOCALES +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.not +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.CopyAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindIntTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindLongTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.changeFont +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.DAY +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.HOUR +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.MINUTE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.MONTH +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.SECOND +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.TIME_ZONE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.UNIX_TIMESTAMP_MILLIS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.UNIX_TIMESTAMP_SECONDS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.DatetimeConverter.ConversionOrigin.YEAR +import java.awt.Font +import java.time.Duration +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.time.format.TextStyle.FULL_STANDALONE +import java.time.temporal.ChronoField +import java.time.temporal.IsoFields +import java.util.Locale + +class DatetimeConverter( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + private val context: DeveloperUiToolContext, +) : DeveloperUiTool(parentDisposable) { + // -- Properties ---------------------------------------------------------- // + + private var selectedTimeZoneId = configuration.register("timeZoneId", ZoneId.systemDefault().id) + private var formattedStandardFormat = + configuration.register("formattedStandardFormat", DEFAULT_FORMATTED_STANDARD_FORMAT) + private var formattedStandardFormatAddOffset = + configuration.register( + "formattedStandardFormatAddOffset", + DEFAULT_FORMATTED_STANDARD_FORMAT_ADD_OFFSET, + ) + private var formattedStandardFormatAddTimeZone = + configuration.register( + "formattedStandardFormatAddTimeZone", + DEFAULT_FORMATTED_STANDARD_FORMAT_ADD_TIME_ZONE, + ) + private var formattedIndividual = + configuration.register("formattedIndividual", DEFAULT_FORMATTED_INDIVIDUAL) + private var formattedLocale = configuration.register("formattedLocale", DEFAULT_FORMATTED_LOCALE) + private var formattedIndividualFormat = + configuration.register("formattedIndividualFormat", DEFAULT_INDIVIDUAL_FORMAT) + + private var formattedStandardFormatPattern = ValueProperty("") + private var formattedText = ValueProperty("No result") + private var dateDetails = ValueProperty("") + + private val currentUnixTimestampUpdateAlarm by lazy { Alarm(parentDisposable) } + private val currentUnixTimestampUpdate: Runnable by lazy { createCurrentUnixTimestampUpdate() } + private val currentUnixTimestampSeconds = + ValueProperty(System.currentTimeMillis().div(1000).toString()) + private val currentUnixTimestampMillis = ValueProperty(System.currentTimeMillis().toString()) + + private val convertAlarm by lazy { Alarm(parentDisposable) } + + private val convertUnixTimeStampSeconds = ValueProperty(0) + private val convertUnixTimeStampMillis = ValueProperty(0) + private val convertDay = ValueProperty(0) + private val convertMonth = ValueProperty(0) + private val convertYear = ValueProperty(0) + private val convertHour = ValueProperty(0) + private val convertMinute = ValueProperty(0) + private val convertSecond = ValueProperty(0) + + // -- Initialization ------------------------------------------------------ // + + init { + // Validate if selected time zone and formatted local is still available + if (!ZoneId.getAvailableZoneIds().contains(selectedTimeZoneId.get())) { + selectedTimeZoneId.set(ZoneId.systemDefault().id) + } + if (!ALL_AVAILABLE_LOCALES.contains(formattedLocale.get())) { + formattedLocale.set(DEFAULT_FORMATTED_LOCALE) + } + } + + // -- Exposed Methods ----------------------------------------------------- // + + override fun Panel.buildUi() { + group("Current Unix Timestamp") { + if (context.prioritizeVerticalLayout) { + row { + buildTimestampLabelUi( + "Seconds:", + currentUnixTimestampSeconds, + TIMESTAMP_SECONDS_CONTENT_DATA_KEY, + ) + } + row { + buildTimestampLabelUi( + "Milliseconds:", + currentUnixTimestampMillis, + TIMESTAMP_MILLIS_CONTENT_DATA_KEY, + ) + } + } else { + row { + buildTimestampLabelUi( + "Seconds:", + currentUnixTimestampSeconds, + TIMESTAMP_SECONDS_CONTENT_DATA_KEY, + ) + buildTimestampLabelUi( + "Milliseconds:", + currentUnixTimestampMillis, + TIMESTAMP_MILLIS_CONTENT_DATA_KEY, + ) + } + } + } + + group("Convert") { + val initialInstant = Instant.ofEpochMilli(System.currentTimeMillis()) + val initialLocalDateTime = LocalDateTime.ofInstant(initialInstant, selectedTimeZoneId()) + + group("Unix Timestamp") { + if (context.prioritizeVerticalLayout) { + row { buildUnixTimeStampSecondsTextFieldUi(initialInstant) } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + .layout(RowLayout.PARENT_GRID) + row { buildUnixTimeStampMillisTextFieldUi(initialInstant) } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + .layout(RowLayout.PARENT_GRID) + row { buildSetToNowButtonUi() } + } else { + row { + buildUnixTimeStampSecondsTextFieldUi(initialInstant) + buildUnixTimeStampMillisTextFieldUi(initialInstant) + buildSetToNowButtonUi() + } + } + } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + group("Date and Time") { + row { + comboBox(ZoneId.getAvailableZoneIds().sorted()) + .label("Time zone:") + .bindItem(selectedTimeZoneId) + .whenItemSelectedFromUi { convert(TIME_ZONE) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, TIME_ZONE) } + } + data class DateField( + val title: String, + val initialValue: Int, + val valueProperty: ValueProperty, + val range: LongRange, + val changeOrigin: ConversionOrigin, + ) + row { + listOf( + DateField( + "Year", + initialLocalDateTime.year, + convertYear, + LongRange(1970, 9999), + YEAR, + ), + DateField( + "Month", + initialLocalDateTime.monthValue, + convertMonth, + LongRange(1, 12), + MONTH, + ), + DateField( + "Day", + initialLocalDateTime.dayOfMonth, + convertDay, + LongRange(1, 31), + DAY, + ), + ) + .forEach { (title, initialValue, valueProperty, range, changeOrigin) -> + textField() + .label("$title:") + .text(initialValue.toString()) + .bindIntTextImproved(valueProperty) + .columns(5) + .validateLongValue(range) + .whenTextChangedFromUi { convert(changeOrigin) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, changeOrigin) } + } + } + .layout(RowLayout.PARENT_GRID) + row { + listOf( + DateField("Hour", initialLocalDateTime.hour, convertHour, LongRange(0, 23), HOUR), + DateField( + "Minute", + initialLocalDateTime.minute, + convertMinute, + LongRange(0, 59), + MINUTE, + ), + DateField( + "Second", + initialLocalDateTime.second, + convertSecond, + LongRange(0, 59), + SECOND, + ), + ) + .forEach { (title, initialValue, valueProperty, range, changeOrigin) -> + textField() + .label("$title:") + .text(initialValue.toString()) + .bindIntTextImproved(valueProperty) + .columns(5) + .validateLongValue(range) + .whenTextChangedFromUi { convert(changeOrigin) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, changeOrigin) } + } + } + .layout(RowLayout.PARENT_GRID) + row { comment("").bindText(dateDetails) } + } + .layout(RowLayout.PARENT_GRID) + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + group("Formatted") { + buttonsGroup { + lateinit var formattedStandardFormatComboBox: ComboBox + row { + radioButton("Standard format:") + .bindSelected(formattedIndividual.not()) + .onChanged { convert(UNIX_TIMESTAMP_MILLIS) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } + .gap(RightGap.SMALL) + formattedStandardFormatComboBox = + comboBox(StandardFormat.entries) + .bindItem(formattedStandardFormat) + .columns(COLUMNS_MEDIUM) + .whenItemSelectedFromUi { + syncFormattedStandardFormatPattern() + convert(UNIX_TIMESTAMP_MILLIS) + } + .enabledIf(formattedIndividual.not()) + .gap(RightGap.SMALL) + .component + .apply { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } + if (!context.prioritizeVerticalLayout) { + buildStandardFormatConfigurationUi(formattedStandardFormatComboBox) + } + } + .layout(RowLayout.PARENT_GRID) + .bottomGap(BottomGap.NONE) + if (context.prioritizeVerticalLayout) { + row { + cell() + buildStandardFormatConfigurationUi(formattedStandardFormatComboBox) + } + .topGap(TopGap.NONE) + .layout(RowLayout.PARENT_GRID) + } + row { + cell() + comment("") + .bindText(formattedStandardFormatPattern) + .enabledIf(formattedIndividual.not()) + } + .topGap(TopGap.NONE) + .layout(RowLayout.PARENT_GRID) + + row { + radioButton("Individual format:") + .bindSelected(formattedIndividual) + .onChanged { convert(UNIX_TIMESTAMP_MILLIS) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } + .gap(RightGap.SMALL) + expandableTextField() + .bindText(formattedIndividualFormat) + .columns(COLUMNS_MEDIUM) + .whenTextChangedFromUi { convert(UNIX_TIMESTAMP_MILLIS) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } + .validationInfo { + try { + if (formattedIndividual.get()) { + DateTimeFormatter.ofPattern(it.text) + } + return@validationInfo null + } catch (_: Exception) { + return@validationInfo ValidationInfo("Invalid individual format", it) + } + } + .enabledIf(formattedIndividual) + } + .layout(RowLayout.PARENT_GRID) + } + + row { + comboBox(ALL_AVAILABLE_LOCALES) + .label("Locale:") + .bindItem(formattedLocale) + .columns(COLUMNS_MEDIUM) + .onChanged { convert(UNIX_TIMESTAMP_MILLIS) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } + } + .layout(RowLayout.PARENT_GRID) + + row { + label("") + .bindText(formattedText) + .changeFont(scale = 1.1f, style = Font.BOLD) + .gap(RightGap.SMALL) + actionButton(CopyAction(FORMATTED_TEXT_DATA_KEY), DatetimeConverter::class.java.name) + } + .topGap(TopGap.SMALL) + } + .topGap(TopGap.NONE) + } + } + + override fun afterBuildUi() { + init() + } + + override fun reset() { + init() + } + + override fun getData(dataId: String): Any? = + when { + TIMESTAMP_SECONDS_CONTENT_DATA_KEY.`is`(dataId) -> + stripHtml(currentUnixTimestampSeconds.get(), false) + TIMESTAMP_MILLIS_CONTENT_DATA_KEY.`is`(dataId) -> + stripHtml(currentUnixTimestampMillis.get(), false) + FORMATTED_TEXT_DATA_KEY.`is`(dataId) -> stripHtml(formattedText.get(), false) + else -> null + } + + override fun activated() { + scheduleCurrentUnixTimestampUpdate(0) + } + + override fun deactivated() { + currentUnixTimestampUpdateAlarm.cancelAllRequests() + } + + // -- Private Methods ----------------------------------------------------- // + + private fun Row.buildSetToNowButtonUi() { + button("Set to Now") { + convertUnixTimeStampMillis.set(System.currentTimeMillis()) + convert(UNIX_TIMESTAMP_MILLIS, 0) + } + } + + private fun Row.buildUnixTimeStampMillisTextFieldUi(initialInstant: Instant) { + textField() + .validateLongValue(LongRange(0, Long.MAX_VALUE)) + .bindLongTextImproved(convertUnixTimeStampMillis) + .label("Milliseconds:") + .text(initialInstant.toEpochMilli().toString()) + .columns(12) + .whenTextChangedFromUi { convert(UNIX_TIMESTAMP_MILLIS) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } + } + + private fun Row.buildUnixTimeStampSecondsTextFieldUi(initialInstant: Instant) { + textField() + .validateLongValue(LongRange(0, Long.MAX_VALUE)) + .bindLongTextImproved(convertUnixTimeStampSeconds) + .label("Seconds:") + .text(initialInstant.epochSecond.toString()) + .columns(12) + .whenTextChangedFromUi { convert(UNIX_TIMESTAMP_SECONDS) } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_SECONDS) } + } + + private fun Row.buildStandardFormatConfigurationUi( + formattedStandardFormatComboBox: ComboBox + ) { + checkBox("Add offset") + .bindSelected(formattedStandardFormatAddOffset) + .whenStateChangedFromUi { + syncFormattedStandardFormatPattern() + convert(UNIX_TIMESTAMP_MILLIS) + } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_SECONDS) } + .enabledIf(formattedIndividual.not()) + .visibleIf(ComboBoxPredicate(formattedStandardFormatComboBox) { it?.supportsOffset == true }) + .gap(RightGap.SMALL) + checkBox("Add time zone") + .bindSelected(formattedStandardFormatAddTimeZone) + .whenStateChangedFromUi { + syncFormattedStandardFormatPattern() + convert(UNIX_TIMESTAMP_MILLIS) + } + .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_SECONDS) } + .enabledIf(formattedIndividual.not()) + .visibleIf( + ComboBoxPredicate(formattedStandardFormatComboBox) { it?.supportsTimeZone == true } + ) + } + + private fun init() { + syncFormattedStandardFormatPattern() + + convertUnixTimeStampMillis.set(System.currentTimeMillis()) + convert(UNIX_TIMESTAMP_MILLIS, 0) + } + + private fun syncFormattedStandardFormatPattern() { + val pattern = + formattedStandardFormat + .get() + .buildPattern( + offset = formattedStandardFormatAddOffset.get(), + timeZone = formattedStandardFormatAddTimeZone.get(), + ) + formattedStandardFormatPattern.set(pattern) + } + + private fun createCurrentUnixTimestampUpdate(): Runnable = Runnable { + currentUnixTimestampSeconds.set( + "${System.currentTimeMillis().div(1000)}" + ) + currentUnixTimestampMillis.set("${System.currentTimeMillis()}") + scheduleCurrentUnixTimestampUpdate() + } + + private fun scheduleCurrentUnixTimestampUpdate( + delayMillis: Long = TIMESTAMP_UPDATE_INTERVAL_MILLIS + ) { + currentUnixTimestampUpdateAlarm.addRequest(currentUnixTimestampUpdate, delayMillis) + } + + private fun convert(conversionOrigin: ConversionOrigin, delayMillis: Long = 100) { + if ( + validate().any { + it.component?.getUserData(CONVERSION_ORIGIN_KEY)?.kind == conversionOrigin.kind + } + ) { + return + } + + // Take a snapshot of the input fields since the alarm gets execute with + // some delay. + val unixTimeStampSeconds = convertUnixTimeStampSeconds.get() + val unixTimeStampMillis = convertUnixTimeStampMillis.get() + + val convert: () -> Unit = { + when (conversionOrigin) { + UNIX_TIMESTAMP_SECONDS -> { + val timestampAtSelectedTimeZone = + Instant.ofEpochSecond(unixTimeStampSeconds).atZone(selectedTimeZoneId()) + setConvertedValues(timestampAtSelectedTimeZone, conversionOrigin) + } + + TIME_ZONE, + UNIX_TIMESTAMP_MILLIS -> { + val timestampAtSelectedTimeZone = + Instant.ofEpochMilli(unixTimeStampMillis).atZone(selectedTimeZoneId()) + setConvertedValues(timestampAtSelectedTimeZone, conversionOrigin) + } + + DAY, + MONTH, + YEAR, + HOUR, + MINUTE, + SECOND -> { + val year = convertYear.get() + val month = convertMonth.get() + val day = convertDay.get() + val hour = convertHour.get() + val minute = convertMinute.get() + val second = convertSecond.get() + + val localDateTime = + ZonedDateTime.of(year, month, day, hour, minute, second, 0, selectedTimeZoneId()) + setConvertedValues(localDateTime, conversionOrigin) + } + } + + // Trigger validation again to show errors from `ErrorHolder`s + validate() + } + if (!isDisposed && !convertAlarm.isDisposed) { + convertAlarm.cancelAllRequests() + convertAlarm.addRequest(convert, delayMillis) + } + } + + private fun setConvertedValues(localDateTime: ZonedDateTime, conversionOrigin: ConversionOrigin) { + val millis = localDateTime.withZoneSameInstant(ZoneOffset.UTC).toInstant().toEpochMilli() + if (conversionOrigin != UNIX_TIMESTAMP_SECONDS) { + convertUnixTimeStampSeconds.set(millis.div(1000)) + } + if (conversionOrigin != UNIX_TIMESTAMP_MILLIS) { + convertUnixTimeStampMillis.set(millis) + } + if (conversionOrigin != YEAR) { + convertYear.set(localDateTime.year) + } + if (conversionOrigin != MONTH) { + convertMonth.set(localDateTime.monthValue) + } + if (conversionOrigin != DAY) { + convertDay.set(localDateTime.dayOfMonth) + } + if (conversionOrigin != HOUR) { + convertHour.set(localDateTime.hour) + } + if (conversionOrigin != MINUTE) { + convertMinute.set(localDateTime.minute) + } + if (conversionOrigin != SECOND) { + convertSecond.set(localDateTime.second) + } + + val dayOfYear = localDateTime.get(ChronoField.DAY_OF_YEAR).toLong() + val weekNumber = localDateTime.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR).toLong() + val quarterOfYear = localDateTime.get(IsoFields.QUARTER_OF_YEAR).toLong() + val dayName = localDateTime.dayOfWeek.getDisplayName(FULL_STANDALONE, Locale.getDefault()) + if (context.prioritizeVerticalLayout) { + dateDetails.set( + "$dayName; ${formatEnglish(dayOfYear)} day of the year; ${formatEnglish(weekNumber)} week; ${ + formatEnglish( + quarterOfYear + ) + } quarter" + ) + } else { + dateDetails.set( + "A $dayName, the ${formatEnglish(dayOfYear)} day of the year, in the ${formatEnglish(weekNumber)} week, within the ${ + formatEnglish( + quarterOfYear + ) + } quarter." + ) + } + + formattedText.set( + "${formatDateTime(localDateTime).ifBlank { "No result" }}" + ) + } + + private fun formatDateTime(localDateTime: ZonedDateTime): String = + try { + val formatter = + if (formattedIndividual.get()) { + DateTimeFormatter.ofPattern(formattedIndividualFormat.get()) + .withLocale(formattedLocale.get().locale) + ?.withZone(selectedTimeZoneId()) + } else { + DateTimeFormatter.ofPattern(formattedStandardFormatPattern.get()) + .withLocale(formattedLocale.get().locale) + .withZone(formattedStandardFormat.get().fixedTimeZone ?: selectedTimeZoneId()) + } + localDateTime.format(formatter) + } catch (e: Exception) { + "Error: ${e.message}" + } + + private fun selectedTimeZoneId(): ZoneId = ZoneId.of(selectedTimeZoneId.get()) + + private fun Row.buildTimestampLabelUi( + title: String, + timestampProperty: ObservableProperty, + contentDataKey: DataKey, + ) { + panel { + row { + label(title).gap(RightGap.SMALL) + label("") + .changeFont(scale = 1.5f, style = Font.BOLD) + .bindText(timestampProperty) + .gap(RightGap.SMALL) + actionButton(CopyAction(contentDataKey)) + } + .topGap(TopGap.NONE) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = "Date and Time", + contentTitle = "Date and Time Converter", + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> DatetimeConverter) = { configuration -> + DatetimeConverter(configuration, parentDisposable, context) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class ConversionOriginKind { + + TIME_ZONE, + UNIX_TIMESTAMP_SECONDS, + UNIX_TIMESTAMP_MILLIS, + LOCAL_DATE_TIME, + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class ConversionOrigin(val kind: ConversionOriginKind) { + + TIME_ZONE(ConversionOriginKind.TIME_ZONE), + UNIX_TIMESTAMP_SECONDS(ConversionOriginKind.UNIX_TIMESTAMP_SECONDS), + UNIX_TIMESTAMP_MILLIS(ConversionOriginKind.UNIX_TIMESTAMP_MILLIS), + DAY(ConversionOriginKind.LOCAL_DATE_TIME), + MONTH(ConversionOriginKind.LOCAL_DATE_TIME), + YEAR(ConversionOriginKind.LOCAL_DATE_TIME), + HOUR(ConversionOriginKind.LOCAL_DATE_TIME), + MINUTE(ConversionOriginKind.LOCAL_DATE_TIME), + SECOND(ConversionOriginKind.LOCAL_DATE_TIME), + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class StandardFormat( + private val title: String, + private val pattern: String, + val supportsOffset: Boolean = true, + val supportsTimeZone: Boolean = true, + val fixedTimeZone: ZoneId? = null, + ) { + + ISO_8601("ISO-8601 date time", "yyyy-MM-dd'T'HH:mm:ss.SSS"), + ISO_8601_UTC( + title = "ISO-8601 date time at UTC", + pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + supportsOffset = false, + supportsTimeZone = false, + fixedTimeZone = ZoneOffset.UTC, + ), + ISO_8601_DATE("ISO-8601 date", "yyyy-MM-dd"), + ISO_8601_TIME_WITH("ISO-8601 time", "HH:mm:ss"), + ISO_8601_ORDINAL_DATE("ISO-8601 ordinal date", "yyyy-DDD"), + ISO_8601_WEEK_DATE("ISO-8601 week date", "YYYY-'W'ww-e"), + RFC_1123_DATE_TIME("RFC-1123 date time", "EEE, dd MMM yyyy HH:mm:ss"); + + fun buildPattern(offset: Boolean, timeZone: Boolean): String { + val patternBuilder = StringBuilder(pattern) + if (offset && supportsOffset) { + patternBuilder.append("XXX") + } + if (timeZone && supportsTimeZone) { + patternBuilder.append("'['VV']'") + } + return patternBuilder.toString() + } + + override fun toString(): String = title + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val TIMESTAMP_SECONDS_CONTENT_DATA_KEY = DataKey.create("timestampSeconds") + private val TIMESTAMP_MILLIS_CONTENT_DATA_KEY = DataKey.create("timestampMillis") + private val FORMATTED_TEXT_DATA_KEY = DataKey.create("formattedText") + + private val TIMESTAMP_UPDATE_INTERVAL_MILLIS: Long = Duration.ofSeconds(1).toMillis() + + private const val DEFAULT_FORMATTED_INDIVIDUAL = false + private val DEFAULT_FORMATTED_LOCALE = LocaleContainer(Locale.getDefault()) + private const val DEFAULT_INDIVIDUAL_FORMAT = "" + private val DEFAULT_FORMATTED_STANDARD_FORMAT = StandardFormat.ISO_8601_UTC + private const val DEFAULT_FORMATTED_STANDARD_FORMAT_ADD_OFFSET = true + private const val DEFAULT_FORMATTED_STANDARD_FORMAT_ADD_TIME_ZONE = false + + private val CONVERSION_ORIGIN_KEY = Key("conversionOrigin") + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EncoderDecoder.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EncoderDecoder.kt new file mode 100644 index 00000000..be978503 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EncoderDecoder.kt @@ -0,0 +1,34 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.BidirectionalConverter +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle + +abstract class EncoderDecoder( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, + title: String, +) : + BidirectionalConverter( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = title, + sourceTitle = UiToolsBundle.message("encoder-decoder.source-title"), + targetTitle = UiToolsBundle.message("encoder-decoder.target-title"), + toSourceTitle = UiToolsBundle.message("encoder-decoder.to-source-title"), + toTargetTitle = UiToolsBundle.message("encoder-decoder.to-target-title"), + ) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EscapeSequencesEscaperUnescaper.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EscapeSequencesEscaperUnescaper.kt new file mode 100644 index 00000000..e7b2b031 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EscapeSequencesEscaperUnescaper.kt @@ -0,0 +1,192 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle + +class EscapeSequencesEscaperUnescaper( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + EncoderDecoder( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("escape-sequences-escaper-unescaper.title"), + ) { + // -- Properties ---------------------------------------------------------- // + + private var lineBreaksEncodingEnabled = configuration.register("lineBreaksEncodingEnabled", true) + private var lineBreaksEncodingEscapeSequence = + configuration.register("lineBreaksEscapeSequence", LineBreak.CRLF) + private var tabsEncodingEnabled = configuration.register("tabsEncodingEnabled", true) + private var backslashsEncodingEnabled = configuration.register("backslashsEncodingEnabled", true) + private var singleQuotesEncodingEnabled = + configuration.register("singleQuotesEncodingEnabled", true) + private var doubleQuotesEncodingEnabled = + configuration.register("doubleQuotesEncodingEnabled", true) + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun doConvertToTarget(source: ByteArray): ByteArray { + var text = String(source) + + if (backslashsEncodingEnabled.get()) { + text = text.replace("\\", "\\\\") + } + + if (lineBreaksEncodingEnabled.get()) { + text = + when (lineBreaksEncodingEscapeSequence.get()) { + LineBreak.CRLF -> StringUtil.convertLineSeparators(text, "\\r\\n") + LineBreak.LF -> StringUtil.convertLineSeparators(text, "\\n") + } + } + + if (tabsEncodingEnabled.get()) { + text = text.replace("\t", "\\t") + } + + if (singleQuotesEncodingEnabled.get()) { + text = text.replace("'", "\\'") + } + + if (doubleQuotesEncodingEnabled.get()) { + text = text.replace("\"", "\\\"") + } + + return text.toByteArray() + } + + /** + * The target input is not depending on the selected line break decoding, because the user can put + * anything into the editor without changing the configuration first. + */ + override fun doConvertToSource(target: ByteArray): ByteArray { + var text = String(target) + + if (lineBreaksEncodingEnabled.get()) { + text = text.replace("\\r\\n", System.lineSeparator()).replace("\\n", System.lineSeparator()) + } + + if (tabsEncodingEnabled.get()) { + text = text.replace("\\t", "\t") + } + + if (backslashsEncodingEnabled.get()) { + text = text.replace("\\\\", "\\") + } + + if (singleQuotesEncodingEnabled.get()) { + text = text.replace("\\'", "'") + } + + if (doubleQuotesEncodingEnabled.get()) { + text = text.replace("\\\"", "\"") + } + + return text.toByteArray() + } + + override fun Panel.buildBottomConfigurationUi() { + collapsibleGroup( + title = UiToolsBundle.message("escape-sequences-escaper-unescaper.settings.title") + ) { + groupRowsRange( + UiToolsBundle.message( + "escape-sequences-escaper-unescaper.settings.escape-sequences-to-be-decoded" + ) + ) { + row { + checkBox( + UiToolsBundle.message("escape-sequences-escaper-unescaper.do-line-break-decoding") + ) + .bindSelected(lineBreaksEncodingEnabled) + .gap(RightGap.SMALL) + comboBox(LineBreak.entries) + .bindItem(lineBreaksEncodingEscapeSequence) + .enabledIf(lineBreaksEncodingEnabled) + } + .layout(RowLayout.PARENT_GRID) + + row { + checkBox(UiToolsBundle.message("escape-sequences-escaper-unescaper.do-tab-decoding")) + .bindSelected(tabsEncodingEnabled) + .gap(RightGap.SMALL) + + checkBox( + UiToolsBundle.message("escape-sequences-escaper-unescaper.do-backslash-decoding") + ) + .bindSelected(backslashsEncodingEnabled) + .gap(RightGap.SMALL) + } + .layout(RowLayout.PARENT_GRID) + + row { + checkBox( + UiToolsBundle.message("escape-sequences-escaper-unescaper.do-single-quote-decoding") + ) + .bindSelected(singleQuotesEncodingEnabled) + .gap(RightGap.SMALL) + + checkBox( + UiToolsBundle.message("escape-sequences-escaper-unescaper.do-double-quote-decoding") + ) + .bindSelected(doubleQuotesEncodingEnabled) + .gap(RightGap.SMALL) + } + .layout(RowLayout.PARENT_GRID) + } + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + private enum class LineBreak(val displayText: String) { + + CRLF("\\r\\n"), + LF("\\n"); + + override fun toString(): String = displayText + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("escape-sequences-escaper-unescaper.title"), + contentTitle = UiToolsBundle.message("escape-sequences-escaper-unescaper.content-title"), + description = + DeveloperUiToolPresentation.contextHelp( + UiToolsBundle.message("escape-sequences-escaper-unescaper.context-help") + ), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> EscapeSequencesEscaperUnescaper) = { configuration -> + EscapeSequencesEscaperUnescaper(configuration, parentDisposable, context, project) + } + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EscaperUnescaper.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EscaperUnescaper.kt new file mode 100644 index 00000000..0308ed0f --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/EscaperUnescaper.kt @@ -0,0 +1,34 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.BidirectionalConverter +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle + +abstract class EscaperUnescaper( + title: String, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + BidirectionalConverter( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = title, + sourceTitle = UiToolsBundle.message("escaper-unescaper.source-title"), + targetTitle = UiToolsBundle.message("escaper-unescaper.target-title"), + toSourceTitle = UiToolsBundle.message("escaper-unescaper.to-source-title"), + toTargetTitle = UiToolsBundle.message("escaper-unescaper.to-target-title"), + ) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/JwtEncoderDecoder.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/JwtEncoderDecoder.kt similarity index 52% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/JwtEncoderDecoder.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/JwtEncoderDecoder.kt index 19430697..4ab4ee63 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/JwtEncoderDecoder.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/JwtEncoderDecoder.kt @@ -1,8 +1,7 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter import ai.grazie.utils.capitalize import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode import com.intellij.icons.AllIcons import com.intellij.json.JsonLanguage @@ -43,37 +42,47 @@ import com.intellij.ui.layout.not import com.intellij.util.Alarm import com.intellij.util.ExceptionUtil import com.intellij.util.text.DateFormatUtil -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor.EditorMode -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.SimpleToggleAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.registerDynamicToolTip -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.setValidationResultBorder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.toPrettyStringWithDefaultObjectMapper -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.ENCODED -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.HEADER_OR_PAYLOAD -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.SIGNATURE_CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.BASE32 -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.BASE64 -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.RAW -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithm.HMAC256 -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithm.entries -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.ECDSA -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.HMAC -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.RSA -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder.StandardClaim.entries -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.SENSITIVE -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import io.ktor.util.* +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.decodeBase64String +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.SENSITIVE +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.Companion.createSensitiveInputsHandlingToolTipText +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.ObjectMapperService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.SimpleToggleAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.registerDynamicToolTip +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.setValidationResultBorder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.ENCODED +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.HEADER_OR_PAYLOAD +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.ChangeOrigin.SIGNATURE_CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.BASE32 +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.BASE64 +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SecretKeyEncodingMode.RAW +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithm.HMAC256 +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.ECDSA +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.HMAC +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder.SignatureAlgorithmKind.RSA +import java.security.Key +import java.security.KeyFactory +import java.security.spec.PKCS8EncodedKeySpec +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Base64 +import java.util.Objects +import java.util.StringJoiner +import javax.swing.Icon +import javax.swing.JComponent import org.apache.commons.codec.binary.Base32 import org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P256_CURVE_AND_SHA256 import org.jose4j.jws.AlgorithmIdentifiers.ECDSA_USING_P384_CURVE_AND_SHA384 @@ -86,23 +95,14 @@ import org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA384 import org.jose4j.jws.AlgorithmIdentifiers.RSA_USING_SHA512 import org.jose4j.jws.JsonWebSignature import org.jose4j.keys.HmacKey -import java.security.Key -import java.security.KeyFactory -import java.security.spec.PKCS8EncodedKeySpec -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.util.* -import javax.swing.Icon -import javax.swing.JComponent -internal class JwtEncoderDecoder( +class JwtEncoderDecoder( private val context: DeveloperUiToolContext, private val configuration: DeveloperToolConfiguration, parentDisposable: Disposable, private val project: Project?, ) : DeveloperUiTool(parentDisposable) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private var liveConversion = configuration.register("liveConversion", true) private var encodedText = configuration.register("encodedText", "", INPUT, EXAMPLE_ENCODED) @@ -114,16 +114,20 @@ internal class JwtEncoderDecoder( private val highlightPayloadAlarm by lazy { Alarm(parentDisposable) } private val conversionAlarm by lazy { Alarm(parentDisposable) } - private var lastActiveInput: DeveloperToolEditor? = null + private var lastActiveInput: AdvancedEditor? = null private val encodedEditor by lazy { createEncodedEditor() } private val headerEditor by lazy { createHeaderEditor() } private val payloadEditor by lazy { createPayloadEditor() } - private val highlightingAttributes by lazy { EditorColorsManager.getInstance().globalScheme.getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES) } + private val highlightingAttributes by lazy { + EditorColorsManager.getInstance() + .globalScheme + .getAttributes(EditorColors.SEARCH_RESULT_ATTRIBUTES) + } private val jwt = Jwt(configuration, encodedText, headerText, payloadText) - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { liveConversion.afterChange(parentDisposable) { handleLiveConversionSwitch() } @@ -135,168 +139,200 @@ internal class JwtEncoderDecoder( } } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // override fun Panel.buildUi() { row { - cell( - Splitter(true, 0.2f).apply { - firstComponent = createEncodedComponent() - secondComponent = createEncodingDecodingComponent() - } - ).align(Align.FILL).resizableColumn() - }.resizableRow() + cell( + Splitter(true, 0.2f).apply { + firstComponent = createEncodedComponent() + secondComponent = createEncodingDecodingComponent() + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() } private fun createEncodedComponent(): JComponent = panel { row { - cell(encodedEditor.component) - .validationOnApply(encodedEditor.bindValidator(jwt.encodedErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .align(Align.FILL) - .resizableColumn() - }.resizableRow().bottomGap(BottomGap.NONE) + cell(encodedEditor.component) + .validationOnApply(encodedEditor.bindValidator(jwt.encodedErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + .bottomGap(BottomGap.NONE) val signatureErrors = jwt.signatureErrorHolder.asComponentPredicate() row { - text(" Valid signature") - .visibleIf(signatureErrors.not()) - .resizableColumn() - text("") - .bindText(jwt.signatureErrorHolder.asPropertyForTextCell()) - .visibleIf(signatureErrors) - .resizableColumn() - }.topGap(TopGap.NONE) + text(" Valid signature") + .visibleIf(signatureErrors.not()) + .resizableColumn() + text("") + .bindText(jwt.signatureErrorHolder.asPropertyForTextCell()) + .visibleIf(signatureErrors) + .resizableColumn() + } + .topGap(TopGap.NONE) } @Suppress("UnstableApiUsage") private fun createEncodingDecodingComponent(): JComponent = panel { row { - val liveConversionCheckBox = checkBox("Live conversion") - .bindSelected(liveConversion) - .gap(RightGap.SMALL) + val liveConversionCheckBox = + checkBox("Live conversion").bindSelected(liveConversion).gap(RightGap.SMALL) button("â–¼ Decode") { convert(ENCODED) } .enabledIf(liveConversionCheckBox.selected.not()) .gap(RightGap.SMALL) button("â–² Encode") { - // This will set the signature algorithm in the header and will run the encoding. - convert(SIGNATURE_CONFIGURATION) - }.enabledIf(liveConversionCheckBox.selected.not()) + // This will set the signature algorithm in the header and will run the encoding. + convert(SIGNATURE_CONFIGURATION) + } + .enabledIf(liveConversionCheckBox.selected.not()) } if (context.prioritizeVerticalLayout) { row { - cell( - Splitter(true, 0.5f).apply { - firstComponent = createHeaderEditorComponent() - secondComponent = createPayloadEditorComponent() - } - ).align(Align.FILL).resizableColumn() - }.resizableRow().bottomGap(BottomGap.NONE) - } - else { + cell( + Splitter(true, 0.3f).apply { + firstComponent = createHeaderEditorComponent() + secondComponent = createPayloadEditorComponent() + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + .bottomGap(BottomGap.NONE) + } else { row { - cell( - Splitter(false, 0.5f).apply { - firstComponent = createHeaderEditorComponent() - secondComponent = createPayloadEditorComponent() - } - ).align(Align.FILL).resizableColumn() - }.resizableRow().bottomGap(BottomGap.NONE) + cell( + Splitter(false, 0.5f).apply { + firstComponent = createHeaderEditorComponent() + secondComponent = createPayloadEditorComponent() + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + .bottomGap(BottomGap.NONE) } collapsibleGroup("Signature Algorithm Configuration") { - lateinit var signatureAlgorithmComboBox: ComboBox - row { - signatureAlgorithmComboBox = comboBox(SignatureAlgorithm.entries) - .label("Algorithm:") - .bindItem(jwt.signature.algorithm) - .whenItemSelectedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } - .component - }.layout(RowLayout.PARENT_GRID).topGap(TopGap.NONE) - - row { - // Bug: The label from `expandableTextField().label(...)` disappears - // if the encoding selection gets changed - label("Secret key:") - expandableTextField() - .align(AlignX.FILL) - .bindText(jwt.signature.secret) - .whenTextChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } - .gap(RightGap.SMALL) - .resizableColumn() - .registerDynamicToolTip({ DeveloperToolsApplicationSettings.instance.createSensitiveInputsHandlingToolTipText() }) - - val encodingActions = mutableListOf().apply { - SecretKeyEncodingMode.entries.forEach { secretKeyEncodingModeValue -> - add(SimpleToggleAction( - text = secretKeyEncodingModeValue.title, - icon = AllIcons.Actions.ToggleSoftWrap, - isSelected = { jwt.signature.secretEncodingMode.get() == secretKeyEncodingModeValue }, - setSelected = { jwt.signature.secretEncodingMode.set(secretKeyEncodingModeValue) } - )) + lateinit var signatureAlgorithmComboBox: ComboBox + row { + signatureAlgorithmComboBox = + comboBox(SignatureAlgorithm.entries) + .label("Algorithm:") + .bindItem(jwt.signature.algorithm) + .whenItemSelectedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } + .component } - } - actionButton( - UiUtils.actionsPopup( - title = "Encoding", - icon = AllIcons.General.Settings, - actions = encodingActions + .layout(RowLayout.PARENT_GRID) + .topGap(TopGap.NONE) + + row { + // Bug: The label from `expandableTextField().label(...)` disappears + // if the encoding selection gets changed + label("Secret key:") + expandableTextField() + .align(AlignX.FILL) + .bindText(jwt.signature.secret) + .whenTextChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } + .gap(RightGap.SMALL) + .resizableColumn() + .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } + + val encodingActions = + mutableListOf().apply { + SecretKeyEncodingMode.entries.forEach { secretKeyEncodingModeValue -> + add( + SimpleToggleAction( + text = secretKeyEncodingModeValue.title, + icon = AllIcons.Actions.ToggleSoftWrap, + isSelected = { + jwt.signature.secretEncodingMode.get() == secretKeyEncodingModeValue + }, + setSelected = { + jwt.signature.secretEncodingMode.set(secretKeyEncodingModeValue) + }, + ) + ) + } + } + actionButton( + UiUtils.actionsPopup( + title = "Encoding", + icon = AllIcons.General.Settings, + actions = encodingActions, + ) + ) + } + .visibleIf(ComboBoxPredicate(signatureAlgorithmComboBox) { it?.kind?.keyFactory == null }) + .layout(RowLayout.PARENT_GRID) + + row { + textArea() + .rows(5) + .align(Align.FILL) + .label(label = "Private key:", position = LabelPosition.TOP) + .bindText(jwt.signature.privateKey) + .setValidationResultBorder() + .whenTextChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } + .validationInfo(jwt.signature.privateKeyErrorHolder.asValidation()) + .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } + } + .visibleIf(ComboBoxPredicate(signatureAlgorithmComboBox) { it?.kind?.keyFactory != null }) + + row { + checkBox("Strict key requirements validation") + .bindSelected(jwt.signature.strictSigningKeyValidation) + .whenStateChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } + .gap(RightGap.SMALL) + contextHelp( + "The RFC 7518 for the JSON Web Algorithms (JWA) specifies some restrictions that a key or secret should fulfill for the computation of a signature (e.g., a minimum length). This option can be used to enforce these restrictions." ) - ) - }.visibleIf(ComboBoxPredicate(signatureAlgorithmComboBox) { it?.kind?.keyFactory == null }).layout(RowLayout.PARENT_GRID) - - row { - textArea() - .rows(5) - .align(Align.FILL) - .label(label = "Private key:", position = LabelPosition.TOP) - .bindText(jwt.signature.privateKey) - .setValidationResultBorder() - .whenTextChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } - .validationInfo(jwt.signature.privateKeyErrorHolder.asValidation()) - .registerDynamicToolTip({ DeveloperToolsApplicationSettings.instance.createSensitiveInputsHandlingToolTipText() }) - }.visibleIf(ComboBoxPredicate(signatureAlgorithmComboBox) { it?.kind?.keyFactory != null }) - - row { - checkBox("Strict key requirements validation") - .bindSelected(jwt.signature.strictSigningKeyValidation) - .whenStateChangedFromUi { convertFromUi(SIGNATURE_CONFIGURATION) } - .gap(RightGap.SMALL) - contextHelp("The RFC 7518 for the JSON Web Algorithms (JWA) specifies some restrictions that a key or secret should fulfill for the computation of a signature (e.g., a minimum length). This option can be used to enforce these restrictions.") + } } - }.apply { expanded = false }.topGap(TopGap.NONE) + .apply { expanded = false } + .topGap(TopGap.NONE) } private fun createPayloadEditorComponent(): JComponent = panel { row { - cell(payloadEditor.component) - .validationOnApply(payloadEditor.bindValidator(jwt.payloadErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .align(Align.FILL) - .resizableColumn() - }.resizableRow() + cell(payloadEditor.component) + .validationOnApply(payloadEditor.bindValidator(jwt.payloadErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() } private fun createHeaderEditorComponent(): JComponent = panel { row { - cell(headerEditor.component) - .validationOnApply(headerEditor.bindValidator(jwt.headerErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .align(Align.FILL) - .resizableColumn() - }.resizableRow() + cell(headerEditor.component) + .validationOnApply(headerEditor.bindValidator(jwt.headerErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() } override fun afterBuildUi() { - convert(HEADER_OR_PAYLOAD) + convertFromUi(ENCODED) } override fun reset() { convert(ENCODED) } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun convertFromUi(changeOrigin: ChangeOrigin) { if (!liveConversion.get()) { @@ -347,7 +383,7 @@ internal class JwtEncoderDecoder( TextRange(dotIndex, dotIndex + 1), ENCODED_DOT_SEPARATOR_HIGHLIGHTER_LAYER, highlightingAttributes, - ENCODED_DOT_SEPARATOR_GROUP_ID + ENCODED_DOT_SEPARATOR_GROUP_ID, ) dotIndex = encoded.indexOf('.', dotIndex + 1) i++ @@ -373,70 +409,82 @@ internal class JwtEncoderDecoder( } } - private fun doHighlightClaims(editor: DeveloperToolEditor) { + private fun doHighlightClaims(editor: AdvancedEditor) { editor.removeTextRangeHighlighters(HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID) UNIX_TIMESTAMP_SECONDS_JSON_VALUE_REGEX.findAll(editor.text).forEach { val unixTimestampSecondsMatch = it.groups[1] if (unixTimestampSecondsMatch != null) { - val textRange = TextRange(unixTimestampSecondsMatch.range.first, unixTimestampSecondsMatch.range.last + 1) + val textRange = + TextRange(unixTimestampSecondsMatch.range.first, unixTimestampSecondsMatch.range.last + 1) editor.highlightTextRange( textRange, UNIX_TIMESTAMP_HIGHLIGHT_LAYER, null, HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID, - ClaimFeatureUnixTimestampGutterIconRenderer(textRange, unixTimestampSecondsMatch.value.toLong()) + ClaimFeatureUnixTimestampGutterIconRenderer( + textRange, + unixTimestampSecondsMatch.value.toLong(), + ), ) } } - CLAIM_REGEX.findAll(editor.text).mapNotNull { - val claimMatch = it.groups[1] - if (claimMatch != null) { - val standardClaim = StandardClaim.findByFieldName(claimMatch.value) - if (standardClaim != null) { - return@mapNotNull claimMatch.range to standardClaim + CLAIM_REGEX.findAll(editor.text) + .mapNotNull { + val claimMatch = it.groups[1] + if (claimMatch != null) { + val standardClaim = StandardClaim.findByFieldName(claimMatch.value) + if (standardClaim != null) { + return@mapNotNull claimMatch.range to standardClaim + } } + null + } + .forEach { (claimRange, standardClaim) -> + val textRange = TextRange(claimRange.first, claimRange.last + 1) + editor.highlightTextRange( + textRange, + CLAIM_REGEX_MATCH_HIGHLIGHT_LAYER, + null, + HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID, + StandardClaimGutterIconRenderer(textRange, standardClaim), + ) } - null - }.forEach { (claimRange, standardClaim) -> - val textRange = TextRange(claimRange.first, claimRange.last + 1) - editor.highlightTextRange( - textRange, - CLAIM_REGEX_MATCH_HIGHLIGHT_LAYER, - null, - HEADER_PAYLOAD_HIGHLIGHT_GROUP_ID, - StandardClaimGutterIconRenderer(textRange, standardClaim) - ) - } } - private fun createEncodedEditor(): DeveloperToolEditor = + private fun createEncodedEditor(): AdvancedEditor = createEditor( - id = "jwt-encoder-decoder-encoded", + id = "encoded", changeOrigin = ENCODED, title = "Encoded", language = PlainTextLanguage.INSTANCE, - textProperty = encodedText - ) { highlightDotSeparator() } + textProperty = encodedText, + ) { + highlightDotSeparator() + } - private fun createHeaderEditor(): DeveloperToolEditor = + private fun createHeaderEditor(): AdvancedEditor = createEditor( - id = "jwt-encoder-decoder-header", + id = "header", changeOrigin = HEADER_OR_PAYLOAD, title = "Header", language = JsonLanguage.INSTANCE, - textProperty = headerText - ) { highlightHeaderClaims() } + textProperty = headerText, + ) { + highlightHeaderClaims() + } - private fun createPayloadEditor(): DeveloperToolEditor = + private fun createPayloadEditor(): AdvancedEditor = createEditor( - id = "jwt-encoder-decoder-payload", + id = "payload", changeOrigin = HEADER_OR_PAYLOAD, title = "Payload", language = JsonLanguage.INSTANCE, - textProperty = payloadText - ) { highlightPayloadClaims() } + textProperty = payloadText, + ) { + highlightPayloadClaims() + } private fun createEditor( id: String, @@ -444,27 +492,27 @@ internal class JwtEncoderDecoder( title: String, language: Language, textProperty: ValueProperty, - onTextChangeFromUi: (() -> Unit)? = null - ) = DeveloperToolEditor( - id = id, - context = context, - configuration = configuration, - project = project, - title = title, - editorMode = EditorMode.INPUT_OUTPUT, - parentDisposable = parentDisposable, - textProperty = textProperty, - initialLanguage = language - ).apply { - onFocusGained { - lastActiveInput = this - } - this.onTextChangeFromUi { _ -> - lastActiveInput = this - convertFromUi(changeOrigin) - onTextChangeFromUi?.invoke() - } - } + onTextChangeFromUi: (() -> Unit)? = null, + ) = + AdvancedEditor( + id = id, + context = context, + configuration = configuration, + project = project, + title = title, + editorMode = EditorMode.INPUT_OUTPUT, + parentDisposable = parentDisposable, + textProperty = textProperty, + initialLanguage = language, + ) + .apply { + onFocusGained { lastActiveInput = this } + this.onTextChangeFromUi { _ -> + lastActiveInput = this + convertFromUi(changeOrigin) + onTextChangeFromUi?.invoke() + } + } private fun handleLiveConversionSwitch() { if (liveConversion.get()) { @@ -472,7 +520,8 @@ internal class JwtEncoderDecoder( // will now be encoded/decoded once during the switch to live mode. when (lastActiveInput) { encodedEditor -> convert(ENCODED) - headerEditor, payloadEditor -> { + headerEditor, + payloadEditor -> { // This will set the signature algorithm in the header and will run the encoding. convert(SIGNATURE_CONFIGURATION) } @@ -482,11 +531,11 @@ internal class JwtEncoderDecoder( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class StandardClaimGutterIconRenderer( private val textRange: TextRange, - private val standardClaim: StandardClaim + private val standardClaim: StandardClaim, ) : GutterIconRenderer(), DumbAware { override fun getTooltipText(): String = standardClaim.toString() @@ -496,8 +545,7 @@ internal class JwtEncoderDecoder( override fun equals(other: Any?): Boolean { return if (other != null && other is StandardClaimGutterIconRenderer) { other.textRange == textRange && other.standardClaim == standardClaim - } - else { + } else { false } } @@ -507,11 +555,11 @@ internal class JwtEncoderDecoder( override fun getAlignment(): Alignment = Alignment.RIGHT } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class ClaimFeatureUnixTimestampGutterIconRenderer( private val textRange: TextRange, - private val unixTimestampSeconds: Long + private val unixTimestampSeconds: Long, ) : GutterIconRenderer(), DumbAware { override fun getTooltipText(): String { @@ -523,7 +571,11 @@ internal class JwtEncoderDecoder( .format(DateTimeFormatter.ISO_ZONED_DATE_TIME) ) - val diff = DateFormatUtil.formatBetweenDates(unixTimestampSeconds.times(1000), System.currentTimeMillis()) + val diff = + DateFormatUtil.formatBetweenDates( + unixTimestampSeconds.times(1000), + System.currentTimeMillis(), + ) tooltipText.add("${diff.capitalize()}.") return tooltipText.toString() @@ -534,8 +586,7 @@ internal class JwtEncoderDecoder( override fun equals(other: Any?): Boolean { return if (other != null && other is ClaimFeatureUnixTimestampGutterIconRenderer) { other.textRange == textRange && other.unixTimestampSeconds == unixTimestampSeconds - } - else { + } else { false } } @@ -547,37 +598,39 @@ internal class JwtEncoderDecoder( companion object { private val clockGutterIcon = - IconManager.getInstance().getIcon("dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter.svg", JwtEncoderDecoder::class.java.classLoader) + IconManager.getInstance() + .getIcon( + "dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter.svg", + JwtEncoderDecoder::class.java.classLoader, + ) } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class ChangeOrigin { ENCODED, HEADER_OR_PAYLOAD, - SIGNATURE_CONFIGURATION + SIGNATURE_CONFIGURATION, } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private enum class SignatureAlgorithmKind( - val keyFactory: KeyFactory? - ) { + enum class SignatureAlgorithmKind(val keyFactory: KeyFactory?) { HMAC(null), RSA(KeyFactory.getInstance("RSA")), - ECDSA(KeyFactory.getInstance("EC")) + ECDSA(KeyFactory.getInstance("EC")), } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private enum class SignatureAlgorithm( + enum class SignatureAlgorithm( val jwtHeaderValue: String, val kind: SignatureAlgorithmKind, @Suppress("unused") // May be used for JWK validation - val algorithmIdentifiers: String + val algorithmIdentifiers: String, ) { HMAC256("HS256", HMAC, HMAC_SHA256), @@ -590,7 +643,6 @@ internal class JwtEncoderDecoder( ECDSA384("ES384", ECDSA, ECDSA_USING_P384_CURVE_AND_SHA384), ECDSA512("ES512", ECDSA, ECDSA_USING_P521_CURVE_AND_SHA512); - override fun toString(): String = "$name ($jwtHeaderValue)" companion object { @@ -600,19 +652,20 @@ internal class JwtEncoderDecoder( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class Jwt( configuration: DeveloperToolConfiguration, val encoded: ValueProperty, val header: ValueProperty, - val payload: ValueProperty + val payload: ValueProperty, ) { val encodedErrorHolder = ErrorHolder() val headerErrorHolder = ErrorHolder() val payloadErrorHolder = ErrorHolder() - val signatureErrorHolder = ErrorHolder(addErrorIconToMessage = true, surroundMessageWithHtml = false) + val signatureErrorHolder = + ErrorHolder(addErrorIconToMessage = true, surroundMessageWithHtml = false) val signature = Signature(configuration, signatureErrorHolder) @@ -624,24 +677,28 @@ internal class JwtEncoderDecoder( // Header if (numOfJwtParts >= 1) { - val handleError: (Exception) -> Unit = { error -> header.set(jwtParts[0]); headerErrorHolder.add(error) } - parseAsJson(jwtParts[0].decodeBase64String(), handleError) { + val handleError: (Exception) -> Unit = { error -> + header.set(jwtParts[0]) + headerErrorHolder.add(error) + } + parseAsJson(text = jwtParts[0], textIsBase64 = true, handleError) { parseHeader(it) - header.set(it.toPrettyStringWithDefaultObjectMapper()) + header.set(ObjectMapperService.instance.prettyPrintJson(it)) } - } - else { + } else { header.set("") } // Payload if (numOfJwtParts >= 2) { - val handleError: (Exception) -> Unit = { error -> payload.set(jwtParts[1]); payloadErrorHolder.add(error) } - parseAsJson(jwtParts[1].decodeBase64String(), handleError) { - payload.set(it.toPrettyStringWithDefaultObjectMapper()) + val handleError: (Exception) -> Unit = { error -> + payload.set(jwtParts[1]) + payloadErrorHolder.add(error) } - } - else { + parseAsJson(text = jwtParts[1], textIsBase64 = true, handleError) { + payload.set(ObjectMapperService.instance.prettyPrintJson(it)) + } + } else { payload.set("") } @@ -650,11 +707,12 @@ internal class JwtEncoderDecoder( signature.compute(jwtParts[0], jwtParts[1])?.let { expectedSignature -> val actualSignature = jwtParts[2] if (expectedSignature != actualSignature) { - signatureErrorHolder.add("Invalid signature. Check the configuration in the 'Signature Algorithm Configuration' section.") + signatureErrorHolder.add( + "Invalid signature. Check the configuration in the 'Signature Algorithm Configuration' section." + ) } } - } - else { + } else { signatureErrorHolder.add("Encoded JWT does not have a signature part") } } @@ -662,45 +720,56 @@ internal class JwtEncoderDecoder( fun encodeJwt() { clearErrorHolders() - val headerJson = try { - val headerJson = objectMapper.readTree(header.get()) - parseHeader(headerJson) - headerJson - } catch (e: Exception) { - headerErrorHolder.add(e) - null - } + val jsonMapper = ObjectMapperService.instance.jsonMapper() - val payloadJson = try { - objectMapper.readTree(payload.get()) - } catch (e: Exception) { - payloadErrorHolder.add(e) - null - } + val headerJson = + try { + val headerJson = jsonMapper.readTree(header.get()) + parseHeader(headerJson) + headerJson + } catch (e: Exception) { + headerErrorHolder.add(e) + null + } + + val payloadJson = + try { + jsonMapper.readTree(payload.get()) + } catch (e: Exception) { + payloadErrorHolder.add(e) + null + } if (headerErrorHolder.isSet() || payloadErrorHolder.isSet()) { encoded.set("") signatureErrorHolder.add("Unable to compute signature due to header or payload errors") - } - else { - val encodedHeader = urlEncoder.encode(objectMapper.writeValueAsString(headerJson!!).encodeToByteArray()).decodeToString() - val encodedPayload = urlEncoder.encode(objectMapper.writeValueAsString(payloadJson!!).encodeToByteArray()).decodeToString() + } else { + val encodedHeader = + urlEncoder + .encode(jsonMapper.writeValueAsString(headerJson!!).encodeToByteArray()) + .decodeToString() + val encodedPayload = + urlEncoder + .encode(jsonMapper.writeValueAsString(payloadJson!!).encodeToByteArray()) + .decodeToString() val encodedSignature = signature.compute(encodedHeader, encodedPayload) if (encodedSignature == null) { encoded.set("") - signatureErrorHolder.addIfEmpty("Unable to compute signature due signature configuration errors") - } - else { + signatureErrorHolder.addIfNoErrors( + "Unable to compute signature due signature configuration errors" + ) + } else { encoded.set("${encodedHeader}.${encodedPayload}.$encodedSignature") } } } fun setAlgorithmInHeader() { - parseAsJson(header.get(), { headerErrorHolder.add(it) }) { headerNode -> + parseAsJson(text = header.get(), textIsBase64 = false, { headerErrorHolder.add(it) }) { + headerNode -> if (headerNode is ObjectNode) { headerNode.put("alg", signature.algorithm.get().jwtHeaderValue) - header.set(headerNode.toPrettyStringWithDefaultObjectMapper()) + header.set(ObjectMapperService.instance.prettyPrintJson(headerNode)) } } } @@ -718,19 +787,23 @@ internal class JwtEncoderDecoder( val algorithm = SignatureAlgorithm.findByJwtHeaderValue(algFieldValue) if (algorithm != null) { signature.algorithm.set(algorithm) - } - else { + } else { headerErrorHolder.add("Unsupported algorithm: '$algFieldValue'") } - } - else { + } else { headerErrorHolder.add("Missing algorithm header field: 'alg'") } } - private fun parseAsJson(text: String, handleError: (Exception) -> Unit, handleResult: (JsonNode) -> Unit) { + private fun parseAsJson( + text: String, + textIsBase64: Boolean, + handleError: (Exception) -> Unit, + handleResult: (JsonNode) -> Unit, + ) { try { - val jsonNode = objectMapper.readTree(text) + val actualText = if (textIsBase64) text.decodeBase64String() else text + val jsonNode = ObjectMapperService.instance.jsonMapper().readTree(actualText) handleResult(jsonNode) } catch (e: Exception) { handleError(e) @@ -738,19 +811,21 @@ internal class JwtEncoderDecoder( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class Signature( configuration: DeveloperToolConfiguration, - private val signatureErrorHolder: ErrorHolder + private val signatureErrorHolder: ErrorHolder, ) { val algorithm = configuration.register("algorithm", DEFAULT_SIGNATURE_ALGORITHM) - val strictSigningKeyValidation = configuration.register("signingKeyValidation", SIGNING_KEY_VALIDATION_DEFAULT, CONFIGURATION) + val strictSigningKeyValidation = + configuration.register("signingKeyValidation", SIGNING_KEY_VALIDATION_DEFAULT, CONFIGURATION) val secret = configuration.register("secret", "", SENSITIVE, EXAMPLE_SECRET) - val privateKey = configuration.registerWithExampleProvider("privateKey", "", SENSITIVE) { - if (algorithm.get().kind == RSA) EXAMPLE_RSA_PRIVATE_KEY else EXAMPLE_EC_PRIVATE_KEY - } + val privateKey = + configuration.registerWithExampleProvider("privateKey", "", SENSITIVE) { + if (algorithm.get().kind == RSA) EXAMPLE_RSA_PRIVATE_KEY else EXAMPLE_EC_PRIVATE_KEY + } val secretEncodingMode = configuration.register("secretKeyEncodingMode", RAW, CONFIGURATION) val privateKeyErrorHolder = ErrorHolder() @@ -769,14 +844,16 @@ internal class JwtEncoderDecoder( return try { val signingKey = createSigningKey() ?: return null - ExtendedJsonWebSignature().apply { - // The algorithm gets derivative from the header property `alg` - setEncodedHeader(encodedHeader) - setEncodedPayload(encodedPayload) - setKey(signingKey) - isDoKeyValidation = strictSigningKeyValidation.get() - sign() - }.encodedSignature + ExtendedJsonWebSignature() + .apply { + // The algorithm gets derivative from the header property `alg` + setEncodedHeader(encodedHeader) + setEncodedPayload(encodedPayload) + setKey(signingKey) + isDoKeyValidation = strictSigningKeyValidation.get() + sign() + } + .encodedSignature } catch (e: Exception) { signatureErrorHolder.add("Failed to compute signature:", ExceptionUtil.getRootCause(e)) null @@ -786,37 +863,39 @@ internal class JwtEncoderDecoder( private fun createSigningKey(): Key? { val signatureAlgorithm = algorithm.get() return when (signatureAlgorithm.kind) { - HMAC -> HmacKey( - when (secretEncodingMode.get()) { - RAW -> secret.get().encodeToByteArray() - BASE32 -> Base32().decode(secret.get()) - BASE64 -> Base64.getDecoder().decode(secret.get()) - } - ) + HMAC -> + HmacKey( + when (secretEncodingMode.get()) { + RAW -> secret.get().encodeToByteArray() + BASE32 -> Base32().decode(secret.get()) + BASE64 -> Base64.getDecoder().decode(secret.get()) + } + ) - RSA, ECDSA -> readPrivateKey(signatureAlgorithm.kind.keyFactory!!) ?: return null + RSA, + ECDSA -> readPrivateKey(signatureAlgorithm.kind.keyFactory!!) ?: return null } } - private fun readPrivateKey(keyFactory: KeyFactory) = try { - val privateKeyValue = privateKey.get() - if (privateKeyValue.isBlank()) { - privateKeyErrorHolder.add("A private key must be provided") + private fun readPrivateKey(keyFactory: KeyFactory) = + try { + val privateKeyValue = privateKey.get() + if (privateKeyValue.isBlank()) { + privateKeyErrorHolder.add("A private key must be provided") + null + } else { + keyFactory.generatePrivate(PKCS8EncodedKeySpec(toRawKey(privateKey.get()))) + } + } catch (e: Exception) { + privateKeyErrorHolder.add(e) null } - else { - keyFactory.generatePrivate(PKCS8EncodedKeySpec(toRawKey(privateKey.get()))) - } - } catch (e: Exception) { - privateKeyErrorHolder.add(e) - null - } private fun toRawKey(keyInput: String): ByteArray = Base64.getDecoder().decode(keyInput.replace(RAW_KEY_REGEX, "")) private fun handleAlgorithmChange() { - if (DeveloperToolsApplicationSettings.instance.loadExamples) { + if (generalSettings.loadExamples.get()) { loadExampleSecrets() } } @@ -845,84 +924,122 @@ internal class JwtEncoderDecoder( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class StandardClaim( val fieldName: String, val title: String, - val description: String + val description: String, ) { - ISSUER("iss", "Issuer", "The issuer of the JWT."), - SUBJECT("sub", "Subject", "The subject of the JWT (e.g., the user)."), - AUDIENCE("aud", "Audience", "The recipient for which the JWT is intended."), - EXPIRATION_TIME("exp", "Expiration Time", "The time after which the JWT expires."), - NOT_BEFORE_TIME("nbf", "Not Before Time", "The time before which the JWT must not be accepted for processing."), - ISSUED_AT_TIME("iat", "Issued at Time", "The time at which the JWT was issued."), - JWT_ID("jti", "JWT ID", "An Unique identifier of this JWT."), - ALG("alg", "Algorithm", "The algorithm to calculate the signature of this JWT."), - AZP("azp", "Authorized Party", "The party to which the JWT was issued."), - SID("sid", "Session ID", "An unique session ID."), - NONCE("nonce", "Nonce", "A value used to associate a client session with this JWT."), - AT_HASH("at_hash", "Access Token Hash Value", "The hash of an access token."), - C_HASH("c_hash", "Code Hash Value", "The hash of a code."), - ACT("act", "Actor", "The has of an access token."); + TYP(fieldName = "typ", title = "Type", description = "Indicating that this token is a JWT."), + ISSUER(fieldName = "iss", title = "Issuer", description = "The issuer of the JWT."), + SUBJECT(fieldName = "sub", title = "Subject", description = "The subject of the JWT."), + AUDIENCE(fieldName = "aud", title = "Audience", description = "The recipient of the JWT."), + EXPIRATION_TIME( + fieldName = "exp", + title = "Expiration Time", + description = "JWT expiration time.", + ), + NOT_BEFORE_TIME( + fieldName = "nbf", + title = "Not Before Time", + description = "JWT valid after this time.", + ), + ISSUED_AT_TIME( + fieldName = "iat", + title = "Issued at Time", + description = "JWT issued at this time.", + ), + JWT_ID(fieldName = "jti", title = "JWT ID", description = "A unique identifier for the JWT."), + ALG( + fieldName = "alg", + title = "Algorithm", + description = "The algorithm to calculate the signature of this JWT.", + ), + AZP( + fieldName = "azp", + title = "Authorized Party", + description = "The party to which the JWT was issued.", + ), + SID(fieldName = "sid", title = "Session ID", description = "An unique session ID."), + NONCE( + fieldName = "nonce", + title = "Nonce", + description = "A value used to associate a client session with this JWT.", + ), + AT_HASH( + fieldName = "at_hash", + title = "Access Token Hash Value", + description = "The hash of an access token.", + ), + C_HASH(fieldName = "c_hash", title = "Code Hash Value", description = "The hash of a code."), + ACT(fieldName = "act", title = "Actor", description = "The has of an access token."), + AUTH_TIME( + fieldName = "auth_time", + title = "Authentication Time", + description = "Time of user authentication.", + ), + SCOPE(fieldName = "scope", title = "Scope", description = "Permissions granted to the token."); override fun toString(): String = "$title ($fieldName)
    $description" companion object { - fun findByFieldName(fieldName: String): StandardClaim? = entries.firstOrNull { it.fieldName == fieldName } + fun findByFieldName(fieldName: String): StandardClaim? = + entries.firstOrNull { it.fieldName == fieldName } } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "JSON Web Token (JWT)", - contentTitle = "JSON Web Token (JWT) Decoder/Encoder" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = "JSON Web Token (JWT)", + contentTitle = "JSON Web Token (JWT) Decoder/Encoder", + ) override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> JwtEncoderDecoder) = - { configuration -> JwtEncoderDecoder(context, configuration, parentDisposable, project) } + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> JwtEncoderDecoder) = { configuration -> + JwtEncoderDecoder(context, configuration, parentDisposable, project) + } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - + // -- Inner Type ---------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class ExtendedJsonWebSignature : JsonWebSignature() { + @Suppress("RedundantVisibilityModifier") // False-positive public override fun setEncodedHeader(encodedHeader: String?) { super.setEncodedHeader(encodedHeader) } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // enum class SecretKeyEncodingMode(val title: String) { RAW("Raw"), BASE32("Base32 Encoded"), - BASE64("Base64 Encoded") + BASE64("Base64 Encoded"), } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { - private val objectMapper = ObjectMapper() private val urlEncoder = Base64.getUrlEncoder().withoutPadding() - private val UNIX_TIMESTAMP_SECONDS_JSON_VALUE_REGEX = Regex(":\\s*(?\\b\\d{1,10}\\b)") - private val CLAIM_REGEX = Regex("\\s?\"(?[a-zA-Z]+)\":") + private val UNIX_TIMESTAMP_SECONDS_JSON_VALUE_REGEX = + Regex(":\\s*(?\\b\\d{1,10}\\b)") + private val CLAIM_REGEX = Regex("\\s?\"(?[a-zA-Z]+)\"\\s?:") private const val UNIX_TIMESTAMP_HIGHLIGHT_LAYER = HighlighterLayer.SELECTION - 1 private const val CLAIM_REGEX_MATCH_HIGHLIGHT_LAYER = UNIX_TIMESTAMP_HIGHLIGHT_LAYER - 1 @@ -936,24 +1053,29 @@ internal class JwtEncoderDecoder( private const val EXAMPLE_ENCODED = "ewogICJ0eXAiOiJKV1QiLAogICJhbGciOiJIUzI1NiIKfQ.ewogICJqdGkiOiI5NjQ5MmQ1OS0wYWQ1LTRjMDAtODkyZC01OTBhZDVhYzAwZjMiLAogICJzdWIiOiIwMTIzNDU2Nzg5IiwKICAibmFtZSI6IkpvaG4gRG9lIiwKICAiaWF0IjoxNjgxMDQwNTE1Cn0.IqeNl3lHSUfPfEYmttvlQp1sH9LpAoPJlUiSv4XPDSE" private const val EXAMPLE_SECRET = "s3cre!" - private val EXAMPLE_HEADER = """ + private val EXAMPLE_HEADER = + """ { "typ":"JWT", "alg":"HS256" } - """.trimIndent() - private val EXAMPLE_PAYLOAD = """ + """ + .trimIndent() + private val EXAMPLE_PAYLOAD = + """ { "jti":"96492d59-0ad5-4c00-892d-590ad5ac00f3", "sub":"0123456789", "name":"John Doe", "iat":1681040515 } - """.trimIndent() + """ + .trimIndent() private const val SIGNING_KEY_VALIDATION_DEFAULT = false - private val EXAMPLE_RSA_PRIVATE_KEY = """ + private val EXAMPLE_RSA_PRIVATE_KEY = + """ -----BEGIN RSA PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDdadLFj3DqaYtpZ1ik6ejpIIAU 2KhFqygTvR6SSS9RmcFQu/vojHWzQUhm8aqrGYVkDXCHvEcyBPcZUlWBczcDwQ5YF8VktRpMxfAI @@ -978,12 +1100,15 @@ pBOjAoGAcw2M22BK3NWOHhJ8EC4p6aUIR96lNcCWE/ij+MWCcRdotLDSDuT1q13C+UTxDZ5PsmDs N/bhCDRZYZoLYo0/h6v4zKBDaX05nVUTCYux0Fo2HGrj5S0bjmgyRcr8+enA3CTzCHZPWZ7ZeADb 0Mbtt/Q4JyOCgwORgXJVQBHxxIQ= -----END RSA PRIVATE KEY----- - """.trimIndent() - private val EXAMPLE_EC_PRIVATE_KEY = """ + """ + .trimIndent() + private val EXAMPLE_EC_PRIVATE_KEY = + """ -----BEGIN EC PRIVATE KEY----- MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCDQ+B6qEzr/M2sql4X+09X9YlYt8BKA HX8Q7/6s4KC3qQ== -----END RSA PRIVATE KEY----- - """.trimIndent() + """ + .trimIndent() } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/TextFormattingConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/TextFormattingConverter.kt new file mode 100644 index 00000000..64f0c2c2 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/TextFormattingConverter.kt @@ -0,0 +1,162 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.intellij.openapi.Disposable +import com.intellij.openapi.fileTypes.PlainTextLanguage +import com.intellij.openapi.project.Project +import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.emptyByteArray +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.ObjectMapperService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.BidirectionalConverter +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.ConversionSideHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.TextInputOutputHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle + +class TextFormattingConverter( + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + project: Project?, +) : + BidirectionalConverter( + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + title = UiToolsBundle.message("code-formatting.title"), + sourceTitle = UiToolsBundle.message("code-formatting.first-title"), + targetTitle = UiToolsBundle.message("code-formatting.second-title"), + toSourceTitle = UiToolsBundle.message("code-formatting.to-first-title"), + toTargetTitle = UiToolsBundle.message("code-formatting.to-source-title"), + ), + DeveloperToolConfiguration.ChangeListener { + // -- Properties ---------------------------------------------------------- // + + private var sourceLanguage = configuration.register("firstLanguage", Language.JSON) + private var targetLanguage = configuration.register("secondLanguage", Language.YAML) + + private lateinit var sourceTextInputOutputHandler: TextInputOutputHandler + private lateinit var targetTextInputOutputHandler: TextInputOutputHandler + + private val codeStyles by lazy { + LanguageCodeStyleSettingsProvider.EP_NAME.extensionList.associate { + it.language.id to it.language + } + } + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun ConversionSideHandler.addSourceTextInputOutputHandler() { + sourceTextInputOutputHandler = addTextInputOutputHandler(defaultSourceInputOutputHandlerId) + } + + override fun ConversionSideHandler.addTargetTextInputOutputHandler() { + targetTextInputOutputHandler = addTextInputOutputHandler(defaultTargetInputOutputHandlerId) + } + + override fun afterBuildUi() { + super.afterBuildUi() + syncLanguages() + } + + override fun configurationChanged(property: ValueProperty) { + super.configurationChanged(property) + syncLanguages() + } + + override fun Panel.buildSourceTopConfigurationUi() { + row { + comboBox(Language.entries) + .label(UiToolsBundle.message("code-formatting.first-language")) + .bindItem(sourceLanguage) + } + } + + override fun Panel.buildTargetTopConfigurationUi() { + row { + comboBox(Language.entries) + .label(UiToolsBundle.message("code-formatting.second-language")) + .bindItem(targetLanguage) + } + } + + override fun doConvertToTarget(source: ByteArray): ByteArray = + if (source.isEmpty()) { + emptyByteArray + } else { + targetLanguage.get().writeAsBytes(sourceLanguage.get().parse(source)) + } + + override fun doConvertToSource(target: ByteArray): ByteArray = + if (target.isEmpty()) { + emptyByteArray + } else { + sourceLanguage.get().writeAsBytes(targetLanguage.get().parse(target)) + } + + // -- Private Methods ----------------------------------------------------- // + + private fun syncLanguages() { + val sourceCodeStyle = codeStyles[sourceLanguage.get().languageId] + sourceTextInputOutputHandler.setLanguage(sourceCodeStyle ?: PlainTextLanguage.INSTANCE) + + val targetCodeStyle = codeStyles[targetLanguage.get().languageId] + targetTextInputOutputHandler.setLanguage(targetCodeStyle ?: PlainTextLanguage.INSTANCE) + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class Language( + val title: String, + val languageId: String, + val objectMapper: (ObjectMapperService) -> ObjectMapper, + ) { + + JSON(UiToolsBundle.message("code-formatting.json-title"), "JSON", { it.jsonMapper() }), + YAML(UiToolsBundle.message("code-formatting.yaml-title"), "YAML", { it.yamlMapper() }), + XML(UiToolsBundle.message("code-formatting.xml-title"), "XML", { it.xmlMapper() }), + TOML(UiToolsBundle.message("code-formatting.toml-title"), "TOML", { it.tomlMapper() }), + PROPERTIES( + UiToolsBundle.message("code-formatting.properties-title"), + "Properties", + { it.javaPropsMapper() }, + ); + + override fun toString(): String = title + + fun parse(text: ByteArray): JsonNode = objectMapper(ObjectMapperService.instance).readTree(text) + + fun writeAsBytes(root: JsonNode): ByteArray = + objectMapper(ObjectMapperService.instance).writeValueAsBytes(root) + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("code-formatting.title"), + contentTitle = UiToolsBundle.message("code-formatting.content-title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> TextFormattingConverter) = { configuration -> + TextFormattingConverter(configuration, parentDisposable, context, project) + } + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/BidirectionalConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/BidirectionalConverter.kt new file mode 100644 index 00000000..73bce0e3 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/BidirectionalConverter.kt @@ -0,0 +1,144 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base + +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.observable.util.or +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.layout.and +import com.intellij.ui.layout.not +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.not +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.PropertyComponentPredicate.Companion.createPredicate +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle + +abstract class BidirectionalConverter( + configuration: DeveloperToolConfiguration, + context: DeveloperUiToolContext, + project: Project?, + title: String, + sourceTitle: String, + private val targetTitle: String, + private val toTargetTitle: String, + private val toSourceTitle: String, + parentDisposable: Disposable, +) : + Converter( + configuration = configuration, + context = context, + project = project, + title = title, + sourceTitle = sourceTitle, + targetTitle = targetTitle, + toTargetTitle = toTargetTitle, + parentDisposable = parentDisposable, + ) { + // -- Properties ---------------------------------------------------------- // + + private lateinit var activeConversionSideHandler: ValueProperty + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun requestLiveConversion() { + if (isLiveConversionAvailable()) { + if (activeConversionSideHandler.get() == sourceConversionSideHandler) { + convertToTarget() + } else { + convertToSource() + } + } + } + + override fun Panel.buildActionsUi() { + activeConversionSideHandler = ValueProperty(sourceConversionSideHandler) + + buttonsGroup { + row { + val liveConversionSupportedPredicate = liveConversionSupported.createPredicate(true) + val sourceConversionSideActivePredicate = + activeConversionSideHandler.createPredicate(sourceConversionSideHandler) + + checkBox(UiToolsBundle.message("converter.live-conversion")) + .bindSelected(liveConversionEnabled) + .gap(RightGap.SMALL) + .visibleIf(liveConversionSupportedPredicate) + + icon(AllIcons.General.ArrowUp) + .visibleIf( + sourceConversionSideActivePredicate.not().and(liveConversionSupportedPredicate) + ) + .gap(RightGap.SMALL) + + icon(AllIcons.General.ArrowDown) + .visibleIf(sourceConversionSideActivePredicate.and(liveConversionSupportedPredicate)) + .gap(RightGap.SMALL) + + button("â–¼ $toTargetTitle") { convertToTarget() } + .enabledIf(liveConversionEnabled.not().or(liveConversionSupported.not())) + .gap(RightGap.SMALL) + button("â–² $toSourceTitle") { convertToSource() } + .enabledIf(liveConversionEnabled.not().or(liveConversionSupported.not())) + + buildAdditionalActionsUi() + } + .enabledIf(conversionEnabled.createPredicate(true)) + .bottomGap(BottomGap.NONE) + .topGap(TopGap.NONE) + } + } + + override fun activeInputOutputHandlerChanged( + conversionSideHandler: ConversionSideHandler, + inputOutputHandler: InputOutputHandler, + ) { + activeConversionSideHandler.set(conversionSideHandler) + } + + final override fun createSourceConversionSideHandler(diffSupport: AdvancedEditor.DiffSupport) = + createConversionSideHandler( + title = sourceTitle, + diffSupport = diffSupport, + liveConversionRequested = { requestLiveConversionToTarget() }, + inputOutputDirection = InputOutputDirection.BIDIRECTIONAL, + ) + + final override fun createTargetConversionSideHandler(diffSupport: AdvancedEditor.DiffSupport) = + createConversionSideHandler( + title = targetTitle, + diffSupport = diffSupport, + liveConversionRequested = { requestLiveConversionToSource() }, + inputOutputDirection = InputOutputDirection.BIDIRECTIONAL, + ) + + fun convertToSource() { + convert( + heavyConversionTitle = toSourceTitle, + doConvert = { source, target -> + target.errorHolder.catchException { + val result = doConvertToSource(target.read()) + source.errorHolder.catchException { source.write(result) } + } + }, + ) + } + + abstract fun doConvertToSource(target: ByteArray): ByteArray + + fun requestLiveConversionToSource() { + if (isLiveConversionAvailable()) { + convertToSource() + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/ConversionSideHandler.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/ConversionSideHandler.kt new file mode 100644 index 00000000..1b2e7c1a --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/ConversionSideHandler.kt @@ -0,0 +1,122 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base + +import com.intellij.lang.Language +import com.intellij.openapi.Disposable +import com.intellij.openapi.fileTypes.PlainTextLanguage +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.getUserData +import com.intellij.openapi.ui.putUserData +import com.intellij.openapi.util.Key +import com.intellij.util.ui.components.BorderLayoutPanel +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.TitledTabbedPane +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.onSelectionChanged +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.TextInputOutputHandler.BytesToTextMode +import javax.swing.JComponent + +class ConversionSideHandler( + private val title: String, + private val configuration: DeveloperToolConfiguration, + private val project: Project?, + private val context: DeveloperUiToolContext, + private val parentDisposable: Disposable, + private val conversionEnabled: ValueProperty, + private val liveConversionRequested: () -> Unit, + private val diffSupport: AdvancedEditor.DiffSupport, + private val inputOutputDirection: InputOutputDirection, +) { + // -- Properties ---------------------------------------------------------- // + + private val inputOutputHandlers = mutableListOf() + lateinit var activeInputOutputHandler: ValueProperty + + private lateinit var component: JComponent + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun createComponent(): JComponent { + component = + BorderLayoutPanel().apply { + check(inputOutputHandlers.isNotEmpty()) + + activeInputOutputHandler = ValueProperty(inputOutputHandlers[0]) + + val tabs = + inputOutputHandlers.map { inputOutputHandler -> + inputOutputHandler.title to + inputOutputHandler.createComponent().apply { + putUserData(inputOutputHandlerUserObjectKey, inputOutputHandler) + } + } + addToCenter( + TitledTabbedPane(title, tabs).apply { + onSelectionChanged { + activeInputOutputHandler.set(it.getUserData(inputOutputHandlerUserObjectKey)!!) + } + } + ) + } + + conversionEnabled.afterChange(parentDisposable) { component.isEnabled = it } + + return component + } + + fun addTextInputOutputHandler( + id: String, + defaultText: String = "", + exampleText: String? = null, + bytesToTextMode: BytesToTextMode = BytesToTextMode.BYTES_TO_CHARACTERS, + initialLanguage: Language = PlainTextLanguage.INSTANCE, + ): TextInputOutputHandler { + val textInputOutputHandler = + TextInputOutputHandler( + id = id, + configuration = configuration, + context = context, + parentDisposable = parentDisposable, + project = project, + focusGained = { activeInputOutputHandler.set(it) }, + liveConversionRequested = { + activeInputOutputHandler.set(it) + liveConversionRequested() + }, + diffSupport = diffSupport, + defaultText = defaultText, + exampleText = exampleText, + inputOutputDirection = inputOutputDirection, + bytesToTextMode = bytesToTextMode, + initialLanguage = initialLanguage, + ) + inputOutputHandlers.add(textInputOutputHandler) + return textInputOutputHandler + } + + fun addFileInputOutputHandler(id: String): FileInputOutputHandler { + val fileInputOutputHandler = + FileInputOutputHandler( + id = id, + configuration = configuration, + context = context, + parentDisposable = parentDisposable, + project = project, + inputOutputDirection = inputOutputDirection, + ) + inputOutputHandlers.add(fileInputOutputHandler) + return fileInputOutputHandler + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val inputOutputHandlerUserObjectKey = + Key.create("inputOutputHandlerUserObject") + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/Converter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/Converter.kt new file mode 100644 index 00000000..aad0b706 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/Converter.kt @@ -0,0 +1,341 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base + +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Splitter +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.util.ui.JBInsets +import com.intellij.util.ui.JBUI +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.ChangeListener +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AsyncTaskExecutor + +abstract class Converter( + private val configuration: DeveloperToolConfiguration, + private val context: DeveloperUiToolContext, + protected val project: Project?, + private val title: String, + protected val sourceTitle: String, + private val targetTitle: String, + private val toTargetTitle: String, + parentDisposable: Disposable, +) : DeveloperUiTool(parentDisposable), ChangeListener { + // -- Properties ---------------------------------------------------------- // + + protected var liveConversionEnabled = configuration.register("liveConversion", true) + + protected val conversionEnabled = ValueProperty(true) + protected val liveConversionSupported = ValueProperty(true) + private val textDiffSupported = ValueProperty(true) + + protected lateinit var sourceConversionSideHandler: ConversionSideHandler + protected lateinit var targetConversionSideHandler: ConversionSideHandler + + private val liveConversionExecutor by lazy { AsyncTaskExecutor.onEdt(parentDisposable) } + + protected open val defaultSourceInputOutputHandlerId: String = "source" + protected open val defaultTargetInputOutputHandlerId: String = "target" + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun configurationChanged(property: ValueProperty) { + requestLiveConversion() + } + + override fun activated() { + configuration.addChangeListener(parentDisposable, this) + } + + override fun deactivated() { + configuration.removeChangeListener(this) + } + + fun convertToTarget() { + convert( + heavyConversionTitle = toTargetTitle, + doConvert = { source, target -> + // Only check visible/enabled components, because there might be + // validation errors in non-active `InputOutputHandler`s. + if (validate(onlyVisibleAndEnabled = true).isNotEmpty()) { + return@convert + } + + source.errorHolder.catchException { + val result = doConvertToTarget(source.read()) + target.errorHolder.catchException { target.write(result) } + } + }, + ) + } + + fun requestLiveConversionToTarget() { + if (isLiveConversionAvailable()) { + convertToTarget() + } + } + + abstract fun requestLiveConversion() + + abstract fun doConvertToTarget(source: ByteArray): ByteArray + + override fun Panel.buildUi() { + row { + cell( + Splitter(true, 0.48f).apply { + firstComponent = com.intellij.ui.dsl.builder.panel { buildSourceUi() } + secondComponent = + com.intellij.ui.dsl.builder.panel { + buildActionsUi() + buildTargetUi() + } + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + + buildBottomConfigurationUi() + } + + abstract fun Panel.buildActionsUi() + + override fun wrapperInsets(): JBInsets = + super.wrapperInsets().let { JBUI.insets(0, it.left, it.bottom, it.right) } + + override fun afterBuildUi() { + syncLiveConversionSupported() + syncTextDiffSupported() + convertToTarget() + } + + open fun ConversionSideHandler.addSourceTextInputOutputHandler() { + addTextInputOutputHandler(id = defaultSourceInputOutputHandlerId) + } + + protected open fun ConversionSideHandler.addTargetTextInputOutputHandler() { + addTextInputOutputHandler(id = defaultTargetInputOutputHandlerId) + } + + protected open fun Panel.buildSourceTopConfigurationUi() { + // Override if needed + } + + protected open fun Panel.buildSourceBottomConfigurationUi() { + // Override if needed + } + + protected open fun Panel.buildTargetTopConfigurationUi() { + // Override if needed + } + + protected open fun Panel.buildBottomConfigurationUi() { + // Override if needed + } + + protected open fun Row.buildAdditionalActionsUi() { + // Override if needed + } + + protected fun isLiveConversionAvailable(): Boolean = + liveConversionEnabled.get() && liveConversionSupported.get() + + // -- Private Methods ----------------------------------------------------- // + + private fun ConversionSideHandler.initSourceConversionSide() { + addSourceTextInputOutputHandler() + addFileInputOutputHandler(defaultSourceInputOutputHandlerId) + } + + private fun ConversionSideHandler.initTargetConversionSide() { + addTargetTextInputOutputHandler() + addFileInputOutputHandler(defaultTargetInputOutputHandlerId) + } + + private fun Panel.buildSourceUi() { + sourceConversionSideHandler = + createSourceConversionSideHandler( + createDiffSupport( + secondTitle = targetTitle, + secondText = { String(targetConversionSideHandler.activeInputOutputHandler.get().read()) }, + ) + ) + with(sourceConversionSideHandler) { + initSourceConversionSide() + + buildSourceTopConfigurationUi() + + row { cell(createComponent()).align(Align.FILL) } + .resizableRow() + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + activeInputOutputHandler.afterChange( + parentDisposable, + createActiveInputOutputHandlerListener(sourceConversionSideHandler), + ) + } + + buildSourceBottomConfigurationUi() + } + + private fun Panel.buildTargetUi() { + targetConversionSideHandler = + createTargetConversionSideHandler( + diffSupport = + createDiffSupport( + secondTitle = sourceTitle, + secondText = { + String(sourceConversionSideHandler.activeInputOutputHandler.get().read()) + }, + ) + ) + with(targetConversionSideHandler) { + initTargetConversionSide() + + buildTargetTopConfigurationUi() + + row { cell(createComponent()).align(Align.FILL) } + .resizableRow() + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + activeInputOutputHandler.afterChange( + parentDisposable, + createActiveInputOutputHandlerListener(targetConversionSideHandler), + ) + } + } + + protected abstract fun createSourceConversionSideHandler( + diffSupport: AdvancedEditor.DiffSupport + ): ConversionSideHandler + + protected abstract fun createTargetConversionSideHandler( + diffSupport: AdvancedEditor.DiffSupport + ): ConversionSideHandler + + protected open fun activeInputOutputHandlerChanged( + conversionSideHandler: ConversionSideHandler, + inputOutputHandler: InputOutputHandler, + ) { + // Override if needed + } + + private fun createActiveInputOutputHandlerListener( + parentConversionSideHandler: ConversionSideHandler + ): (InputOutputHandler) -> Unit = { + activeInputOutputHandlerChanged(parentConversionSideHandler, it) + syncLiveConversionSupported() + syncTextDiffSupported() + requestLiveConversion() + } + + private fun createDiffSupport(secondTitle: String, secondText: () -> String) = + AdvancedEditor.DiffSupport( + title = title, + secondTitle = secondTitle, + enabled = textDiffSupported, + secondText = secondText, + ) + + private fun doValidate() { + ApplicationManager.getApplication().invokeLater { + // The `validate` in this class is not used as a validation mechanism. We + // make use of its text field error UI to display the `errorHolder`. + validate() + } + } + + protected fun createConversionSideHandler( + title: String, + diffSupport: AdvancedEditor.DiffSupport, + liveConversionRequested: () -> Unit, + inputOutputDirection: InputOutputDirection, + ) = + ConversionSideHandler( + title = title, + configuration = configuration, + project = project, + context = context, + parentDisposable = parentDisposable, + conversionEnabled = conversionEnabled, + liveConversionRequested = liveConversionRequested, + diffSupport = diffSupport, + inputOutputDirection = inputOutputDirection, + ) + + private fun syncLiveConversionSupported() { + liveConversionSupported.set( + sourceConversionSideHandler.activeInputOutputHandler.get().liveConversionSupported && + targetConversionSideHandler.activeInputOutputHandler.get().liveConversionSupported + ) + } + + private fun syncTextDiffSupported() { + textDiffSupported.set( + sourceConversionSideHandler.activeInputOutputHandler.get().textDiffSupported && + targetConversionSideHandler.activeInputOutputHandler.get().textDiffSupported + ) + } + + protected fun convert( + heavyConversionTitle: String, + doConvert: (InputOutputHandler, InputOutputHandler) -> Unit, + ) { + val sourceInputOutputHandler: InputOutputHandler = + sourceConversionSideHandler.activeInputOutputHandler.get() + val targetInputOutputHandler: InputOutputHandler = + targetConversionSideHandler.activeInputOutputHandler.get() + + sourceInputOutputHandler.errorHolder.clear() + targetInputOutputHandler.errorHolder.clear() + + if ( + sourceInputOutputHandler.liveConversionSupported && + targetInputOutputHandler.liveConversionSupported + ) { + if (!liveConversionExecutor.isDisposed) { + liveConversionExecutor.cancelAll() + liveConversionExecutor.enqueueTask( + { + doConvert(sourceInputOutputHandler, targetInputOutputHandler) + doValidate() + }, + 100, + ) + } + return + } + + conversionEnabled.set(false) + object : Task.Backgroundable(project, heavyConversionTitle) { + + override fun run(indicator: ProgressIndicator) { + indicator.text = heavyConversionTitle + doConvert(sourceInputOutputHandler, targetInputOutputHandler) + } + + override fun onFinished() { + conversionEnabled.set(true) + doValidate() + } + } + .queue() + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/FileInputOutputHandler.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/FileInputOutputHandler.kt new file mode 100644 index 00000000..e89990f5 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/FileInputOutputHandler.kt @@ -0,0 +1,63 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.FileHandling +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.FileHandling.WriteFormat +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import javax.swing.JComponent + +class FileInputOutputHandler( + id: String, + val configuration: DeveloperToolConfiguration, + val context: DeveloperUiToolContext, + val parentDisposable: Disposable, + val project: Project?, + inputOutputDirection: InputOutputDirection, +) : + InputOutputHandler( + id = id, + title = UiToolsBundle.message("converter.file-input-output-handler.title"), + errorHolder = ErrorHolder(), + liveConversionSupported = false, + textDiffSupported = false, + inputOutputDirection = inputOutputDirection, + ) { + // -- Properties ---------------------------------------------------------- // + + private lateinit var fileHandling: FileHandling + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun createComponent(): JComponent { + fileHandling = + FileHandling( + project = project, + file = configuration.register("${id}File", "", INPUT), + writeFormat = + configuration.register("${id}FileWriteFormat", WriteFormat.BINARY, CONFIGURATION), + supportsWrite = inputOutputDirection.supportsWrite, + ) + return fileHandling.crateComponent(errorHolder) + } + + override fun read(): ByteArray { + check(inputOutputDirection.supportsRead) + return fileHandling.readFromFile() + } + + override fun write(output: ByteArray) { + check(inputOutputDirection.supportsWrite) + fileHandling.writeToFile(output) + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/InputOutputDirection.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/InputOutputDirection.kt new file mode 100644 index 00000000..627c620e --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/InputOutputDirection.kt @@ -0,0 +1,16 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base + +enum class InputOutputDirection(val supportsWrite: Boolean, val supportsRead: Boolean) { + // -- Values ------------------------------------------------------------- // + + UNDIRECTIONAL_WRITE(supportsWrite = true, supportsRead = false), + UNDIRECTIONAL_READ(supportsWrite = false, supportsRead = true), + BIDIRECTIONAL(supportsWrite = true, supportsRead = true), + + // -- Properties --------------------------------------------------------- // + // -- Initialization ----------------------------------------------------- // + // -- Exported Methods --------------------------------------------------- // + // -- Private Methods ---------------------------------------------------- // + // -- Inner Type --------------------------------------------------------- // + // -- Companion Object --------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/InputOutputHandler.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/InputOutputHandler.kt new file mode 100644 index 00000000..db3c3741 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/InputOutputHandler.kt @@ -0,0 +1,27 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base + +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import javax.swing.JComponent + +abstract class InputOutputHandler( + val id: String, + val title: String, + val errorHolder: ErrorHolder, + val liveConversionSupported: Boolean, + val textDiffSupported: Boolean, + val inputOutputDirection: InputOutputDirection, +) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + abstract fun createComponent(): JComponent + + abstract fun read(): ByteArray + + abstract fun write(output: ByteArray) + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/TextInputOutputHandler.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/TextInputOutputHandler.kt new file mode 100644 index 00000000..87318eeb --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/TextInputOutputHandler.kt @@ -0,0 +1,129 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base + +import com.intellij.lang.Language +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.fileTypes.PlainTextLanguage +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.panel +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.toHexString +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool.Companion.DUMMY_DIALOG_VALIDATION_REQUESTOR +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import javax.swing.JComponent + +class TextInputOutputHandler( + id: String, + private val configuration: DeveloperToolConfiguration, + private val context: DeveloperUiToolContext, + private val parentDisposable: Disposable, + private val project: Project?, + private val focusGained: (TextInputOutputHandler) -> Unit, + private val liveConversionRequested: (TextInputOutputHandler) -> Unit, + private val diffSupport: AdvancedEditor.DiffSupport? = null, + defaultText: String = "", + exampleText: String? = null, + inputOutputDirection: InputOutputDirection, + private val initialLanguage: Language = PlainTextLanguage.INSTANCE, + private val bytesToTextMode: BytesToTextMode = BytesToTextMode.BYTES_TO_CHARACTERS, +) : + InputOutputHandler( + id = id, + title = + when (bytesToTextMode) { + BytesToTextMode.BYTES_TO_CHARACTERS -> + UiToolsBundle.message("converter.text-input-output-handler.simple-title") + BytesToTextMode.BYTES_TO_HEX -> + UiToolsBundle.message("converter.text-input-output-handler.hex-title") + }, + errorHolder = ErrorHolder(), + liveConversionSupported = true, + textDiffSupported = true, + inputOutputDirection = inputOutputDirection, + ) { + // -- Properties ---------------------------------------------------------- // + + private var inputOutputText = + if (inputOutputDirection.supportsRead) + configuration.register("${id}Text", defaultText, INPUT, exampleText) + else ValueProperty("") + + private lateinit var advancedEditor: AdvancedEditor + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun createComponent(): JComponent = panel { + advancedEditor = + AdvancedEditor( + id = id, + title = null, + editorMode = + when (inputOutputDirection) { + InputOutputDirection.UNDIRECTIONAL_WRITE -> EditorMode.OUTPUT + InputOutputDirection.UNDIRECTIONAL_READ -> EditorMode.INPUT + InputOutputDirection.BIDIRECTIONAL -> EditorMode.INPUT_OUTPUT + }, + parentDisposable = parentDisposable, + configuration = configuration, + context = context, + project = project, + textProperty = inputOutputText, + diffSupport = diffSupport, + initialLanguage = initialLanguage, + ) + .apply { + onFocusGained { focusGained(this@TextInputOutputHandler) } + onTextChangeFromUi { + focusGained(this@TextInputOutputHandler) + liveConversionRequested(this@TextInputOutputHandler) + } + } + + row { + cell(advancedEditor.component) + .validationOnApply(advancedEditor.bindValidator(errorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + } + + override fun read(): ByteArray { + check(inputOutputDirection.supportsRead) + return inputOutputText.get().toByteArray() + } + + override fun write(output: ByteArray) { + check(inputOutputDirection.supportsWrite) + val text = + when (bytesToTextMode) { + BytesToTextMode.BYTES_TO_CHARACTERS -> String(output) + BytesToTextMode.BYTES_TO_HEX -> output.toHexString() + } + ApplicationManager.getApplication().invokeLater { inputOutputText.set(text) } + } + + fun setLanguage(language: Language) { + advancedEditor.language = language + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + enum class BytesToTextMode { + + BYTES_TO_CHARACTERS, + BYTES_TO_HEX, + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/UndirectionalConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/UndirectionalConverter.kt new file mode 100644 index 00000000..dc6e804e --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/base/UndirectionalConverter.kt @@ -0,0 +1,88 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base + +import com.intellij.openapi.Disposable +import com.intellij.openapi.observable.util.or +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindSelected +import dev.turingcomplete.intellijdevelopertoolsplugin.common.not +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.PropertyComponentPredicate.Companion.createPredicate +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle + +abstract class UndirectionalConverter( + configuration: DeveloperToolConfiguration, + context: DeveloperUiToolContext, + project: Project?, + title: String, + sourceTitle: String, + private val targetTitle: String, + private val toTargetTitle: String, + parentDisposable: Disposable, +) : + Converter( + configuration = configuration, + context = context, + project = project, + title = title, + sourceTitle = sourceTitle, + targetTitle = targetTitle, + toTargetTitle = toTargetTitle, + parentDisposable = parentDisposable, + ) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun requestLiveConversion() { + if (isLiveConversionAvailable()) { + convertToTarget() + } + } + + override fun Panel.buildActionsUi() { + buttonsGroup { + row { + val liveConversionSupportedPredicate = liveConversionSupported.createPredicate(true) + + checkBox(UiToolsBundle.message("converter.live-conversion")) + .bindSelected(liveConversionEnabled) + .visibleIf(liveConversionSupportedPredicate) + + button("â–¼ $toTargetTitle") { convertToTarget() } + .enabledIf(liveConversionEnabled.not().or(liveConversionSupported.not())) + .gap(RightGap.SMALL) + + buildAdditionalActionsUi() + } + .enabledIf(conversionEnabled.createPredicate(true)) + .bottomGap(BottomGap.NONE) + .topGap(TopGap.NONE) + } + } + + final override fun createSourceConversionSideHandler(diffSupport: AdvancedEditor.DiffSupport) = + createConversionSideHandler( + title = sourceTitle, + diffSupport = diffSupport, + liveConversionRequested = { requestLiveConversionToTarget() }, + inputOutputDirection = InputOutputDirection.UNDIRECTIONAL_READ, + ) + + final override fun createTargetConversionSideHandler(diffSupport: AdvancedEditor.DiffSupport) = + createConversionSideHandler( + title = targetTitle, + diffSupport = diffSupport, + liveConversionRequested = { /* nothing to do */ }, + inputOutputDirection = InputOutputDirection.UNDIRECTIONAL_WRITE, + ) + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/BaseConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/BaseConverter.kt new file mode 100644 index 00000000..d8273539 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/BaseConverter.kt @@ -0,0 +1,177 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter + +import com.intellij.openapi.Disposable +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.not +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import java.math.BigInteger + +class BaseConverter(configuration: DeveloperToolConfiguration, parentDisposable: Disposable) : + UnitConverter(parentDisposable, "Base") { + // -- Properties ---------------------------------------------------------- // + + private val baseTwoInput = + configuration.register( + key = "${CONFIGURATION_KEY_PREFIX}baseTwoInput", + defaultValue = "0", + propertyType = INPUT, + example = EXAMPLE_BASE_TWO_INPUT, + ) + private val showOnlyCommonBases = + configuration.register("${CONFIGURATION_KEY_PREFIX}showOnlyCommonBases", true, CONFIGURATION) + + private lateinit var baseProperties: Map> + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @Suppress("UnstableApiUsage") + override fun Panel.buildUi() { + baseProperties = + bases + .map { (base, title) -> + val property = if (base == 2) baseTwoInput else ValueProperty("0") + row { + lateinit var textField: JBTextField + val textFieldCell = + textField() + .validate(base) + .label("$title ($base):") + .bindText(property) + .whenTextChangedFromUi { convertFromCommonBase(base, textField) } + .resizableColumn() + .align(Align.FILL) + .gap(RightGap.SMALL) + if (!commonBases.contains(base)) { + textFieldCell.visibleIf(showOnlyCommonBases.not()) + } + textField = textFieldCell.component + } + .layout(RowLayout.PARENT_GRID) + + base to property + } + .toMap() + } + + override fun Panel.buildSettingsUi() { + collapsibleGroup("Settings") { + row { checkBox("Show only common bases").bindSelected(showOnlyCommonBases) } + } + .topGap(TopGap.NONE) + } + + override fun sync() { + convertFromCommonBase(2, null) + } + + // -- Private Methods ----------------------------------------------------- // + + private fun convertFromCommonBase(inputBase: Int, textField: JBTextField?) { + if (textField != null && validate().any { it.component == textField }) { + return + } + + val inputProperty = + baseProperties[inputBase] ?: throw IllegalArgumentException("Unknown base: $inputBase") + val input = inputProperty.get().parse(inputBase) ?: return + + baseProperties + .filter { it.value != inputProperty } + .forEach { (base, property) -> property.set(input.toString(base).uppercase()) } + } + + @Suppress("UnstableApiUsage") + private fun Cell.validate(base: Int) = + this.apply { + validationInfo { + if (!this@validate.component.isEnabled) { + return@validationInfo null + } + try { + this@validate.component.text.parse(base) + ?: return@validationInfo error("Please enter a number") + return@validationInfo null + } catch (_: Exception) { + return@validationInfo error("Please enter a valid number") + } + } + } + + private fun String.parse(base: Int): BigInteger? { + if (this.isBlank()) { + return null + } + + val maxAllowedChar = validChars[base - 1] + val isValid = this.uppercase().all { it in '0'..maxAllowedChar } + if (!isValid) { + throw IllegalArgumentException("Invalid input for base $base") + } + + return BigInteger(this, base) + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val CONFIGURATION_KEY_PREFIX = "baseConverter_" + private const val EXAMPLE_BASE_TWO_INPUT = "10101010" + + private val validChars = ('0'..'9') + ('A'..'Z') + + private val commonBases = setOf(2, 8, 10, 16) + private val bases = + linkedMapOf( + 2 to "Binary", + 3 to "Ternary", + 4 to "Quaternary", + 5 to "Quinary", + 6 to "Senary", + 7 to "Septenary", + 8 to "Octal", + 9 to "Nonary", + 10 to "Decimal", + 11 to "Undecimal", + 12 to "Duodecimal", + 13 to "Tridecimal", + 14 to "Tetradecimal", + 15 to "Pentadecimal", + 16 to "Hexadecimal", + 17 to "Heptadecimal", + 18 to "Octodecimal", + 19 to "Enneadecimal", + 20 to "Vigesimal", + 21 to "Unvigesimal", + 22 to "Duovigesimal", + 23 to "Trivigesimal", + 24 to "Tetravigesimal", + 25 to "Pentavigesimal", + 26 to "Hexavigesimal", + 27 to "Septemvigesimal", + 28 to "Octovigesimal", + 29 to "Ennevigesimal", + 30 to "Trigesimal", + 31 to "Untrigesimal", + 32 to "Duotrigesimal", + 33 to "Tretrigesimal", + 34 to "Tetratrigesimal", + 35 to "Pentatrigesimal", + 36 to "Hexatrigesimal", + ) + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/DataSizeConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/DataSizeConverter.kt new file mode 100644 index 00000000..6e957055 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/DataSizeConverter.kt @@ -0,0 +1,165 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter + +import com.intellij.openapi.Disposable +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.whenStateChangedFromUi +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateBigDecimalValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.DataUnit +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.NumberSystem +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.bitDataUnit +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.dataUnits +import java.math.BigDecimal +import java.math.BigDecimal.ZERO + +class DataSizeConverter(configuration: DeveloperToolConfiguration, parentDisposable: Disposable) : + MathContextUnitConverter(CONFIGURATION_KEY_PREFIX, configuration, parentDisposable, "Data Size") { + // -- Properties ---------------------------------------------------------- // + + private val bitDataSizeValue = + configuration.register( + "${CONFIGURATION_KEY_PREFIX}bitDataSizeValue", + ZERO, + INPUT, + DEFAULT_BIT_DATA_SIZE_VALUE, + ) + private val showLargeDataUnits = + configuration.register( + "${CONFIGURATION_KEY_PREFIX}showLargeDataUnits", + DEFAULT_SHOW_LARGE_DATA_UNITS, + ) + + private val dataSizeProperties: List = createDataProperties() + private val bitDataSizeProperty = dataSizeProperties.first { it.dataUnit == bitDataUnit } + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @Suppress("UnstableApiUsage") + override fun Panel.buildUi() { + NumberSystem.entries.forEach { numberSystem -> + group(numberSystem.title, true) { + dataSizeProperties + .filter { it.dataUnit.numberSystem == numberSystem } + .forEach { transferRateDataUnitProperty -> + row { + val formattedLabel = + label(transferRateDataUnitProperty.inputTitle).gap(RightGap.SMALL) + lateinit var formattedFieldTextField: Cell + formattedFieldTextField = + textField() + .bindText(transferRateDataUnitProperty.formattedValue) + .validateBigDecimalValue(ZERO, mathContext) { it.parseBigDecimal() } + .resizableColumn() + .align(Align.FILL) + .whenTextChangedFromUi { + convertByInputFieldChange( + formattedFieldTextField.component, + transferRateDataUnitProperty, + ) + } + + if (transferRateDataUnitProperty.dataUnit.isLarge) { + formattedLabel.visibleIf(showLargeDataUnits) + formattedFieldTextField.visibleIf(showLargeDataUnits) + } + } + .layout(RowLayout.PARENT_GRID) + } + } + .bottomGap(BottomGap.NONE) + .topGap(TopGap.NONE) + } + } + + private fun convertByInputFieldChange( + inputFieldComponent: JBTextField?, + inputDataSizeProperty: DataSizeProperty, + ) { + if (inputFieldComponent != null && validate().any { it.component == inputFieldComponent }) { + return + } + + if (inputDataSizeProperty != bitDataSizeProperty) { + val inputValue = inputDataSizeProperty.formattedValue.get().parseBigDecimal() + bitDataSizeValue.set(inputDataSizeProperty.dataUnit.toBits(inputValue, mathContext)) + } else { + bitDataSizeValue.set(bitDataSizeProperty.formattedValue.get().parseBigDecimal()) + } + + dataSizeProperties + .filter { it != inputDataSizeProperty } + .forEach { it.setFromBits(bitDataSizeValue.get(), this) } + } + + override fun doSync() { + // During a reset, only the `bitDataSizeValue` will be changed but + // `convertByInputFieldChange` would overwrite the value with the old + // formatted value. + bitDataSizeProperty.formattedValue.set(bitDataSizeValue.get().toFormatted()) + + convertByInputFieldChange(null, bitDataSizeProperty) + } + + @Suppress("UnstableApiUsage") + override fun Panel.buildAdditionalSettingsUi() { + row { + checkBox("Show large data units").bindSelected(showLargeDataUnits).whenStateChangedFromUi { + sync() + } + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun createDataProperties() = + dataUnits.map { + if (it == bitDataUnit) { + DataSizeProperty(it, bitDataSizeValue).apply { + formattedValue.set(bitDataSizeValue.get().toFormatted()) + } + } else { + DataSizeProperty(it, null) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class DataSizeProperty( + val dataUnit: DataUnit, + val rawValueReference: ValueProperty? = null, + ) { + + val formattedValue: ValueProperty = ValueProperty("0") + var inputTitle: String = "${dataUnit.name}:" + + fun setFromBits(bits: BigDecimal, unitConverter: MathContextUnitConverter) { + val result = dataUnit.fromBits(bits, unitConverter.mathContext) + formattedValue.set(with(unitConverter) { result.toFormatted() }) + rawValueReference?.set(result) + } + + override fun toString(): String = dataUnit.name + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val CONFIGURATION_KEY_PREFIX = "dataSizeConverter_" + private val DEFAULT_BIT_DATA_SIZE_VALUE = BigDecimal(1073740000) + private const val DEFAULT_SHOW_LARGE_DATA_UNITS = false + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/DataUnit.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/DataUnit.kt new file mode 100644 index 00000000..644d92c0 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/DataUnit.kt @@ -0,0 +1,210 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter + +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.BaseDataUnit.BIT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.BaseDataUnit.BYTE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.NumberSystem.BASIC +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.NumberSystem.BINARY +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.NumberSystem.DECIMAL +import java.math.BigDecimal +import java.math.MathContext + +object DataUnits { + // -- Properties ---------------------------------------------------------- // + + private val bigDecimalEight = BigDecimal.valueOf(8) + + val bitDataUnit = + DataUnit( + name = "Bit", + abbreviation = "b", + isLarge = false, + baseDataUnit = BIT, + numberSystem = BASIC, + exponent = 0, + ) + + val dataUnits: List = + listOf( + listOf( + bitDataUnit, + DataUnit( + name = "Byte", + abbreviation = "B", + isLarge = false, + baseDataUnit = BIT, + numberSystem = BASIC, + exponent = 3, + ), + ), + createDataUnits( + decimalPrefix = "Kilo", + binaryPrefix = "Kibi", + decimalAbbreviationFirstLetter = 'k', + binaryAbbreviationFirstLetter = 'K', + decimalExponent = 3, + binaryExponent = 10, + isLarge = false, + ), + createDataUnits( + decimalPrefix = "Mega", + binaryPrefix = "Mebi", + decimalAbbreviationFirstLetter = 'M', + binaryAbbreviationFirstLetter = 'M', + decimalExponent = 6, + binaryExponent = 20, + isLarge = false, + ), + createDataUnits( + decimalPrefix = "Giga", + binaryPrefix = "Gibi", + decimalAbbreviationFirstLetter = 'G', + binaryAbbreviationFirstLetter = 'G', + decimalExponent = 9, + binaryExponent = 30, + isLarge = false, + ), + createDataUnits( + decimalPrefix = "Tera", + binaryPrefix = "Tebi", + decimalAbbreviationFirstLetter = 'T', + binaryAbbreviationFirstLetter = 'T', + decimalExponent = 12, + binaryExponent = 40, + isLarge = false, + ), + createDataUnits( + decimalPrefix = "Peta", + binaryPrefix = "Pebi", + decimalAbbreviationFirstLetter = 'P', + binaryAbbreviationFirstLetter = 'P', + decimalExponent = 15, + binaryExponent = 50, + isLarge = true, + ), + createDataUnits( + decimalPrefix = "Exa", + binaryPrefix = "Exbi", + decimalAbbreviationFirstLetter = 'E', + binaryAbbreviationFirstLetter = 'E', + decimalExponent = 18, + binaryExponent = 60, + isLarge = true, + ), + createDataUnits( + decimalPrefix = "Zetta", + binaryPrefix = "Zebi", + decimalAbbreviationFirstLetter = 'Z', + binaryAbbreviationFirstLetter = 'Z', + decimalExponent = 21, + binaryExponent = 70, + isLarge = true, + ), + createDataUnits( + decimalPrefix = "Yotta", + binaryPrefix = "Yobi", + decimalAbbreviationFirstLetter = 'Y', + binaryAbbreviationFirstLetter = 'Y', + decimalExponent = 24, + binaryExponent = 80, + isLarge = true, + ), + ) + .flatten() + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + + private fun createDataUnits( + decimalPrefix: String, + binaryPrefix: String, + decimalAbbreviationFirstLetter: Char, + binaryAbbreviationFirstLetter: Char, + decimalExponent: Int, + binaryExponent: Int, + isLarge: Boolean, + ): List = + listOf( + DataUnit( + "${decimalPrefix}bit", + "${decimalAbbreviationFirstLetter}b", + isLarge, + BIT, + DECIMAL, + decimalExponent, + ), + DataUnit( + "${binaryPrefix}bit", + "${decimalAbbreviationFirstLetter}ib", + isLarge, + BIT, + BINARY, + binaryExponent, + ), + DataUnit( + "${decimalPrefix}byte", + "${binaryAbbreviationFirstLetter}B", + isLarge, + BYTE, + DECIMAL, + decimalExponent, + ), + DataUnit( + "${binaryPrefix}byte", + "${binaryAbbreviationFirstLetter}iB", + isLarge, + BYTE, + BINARY, + binaryExponent, + ), + ) + + // -- Inner Type ---------------------------------------------------------- // + + enum class NumberSystem(val title: String, val base: BigDecimal) { + + BASIC("Basic", BigDecimal.valueOf(2)), + BINARY("Binary", BigDecimal.valueOf(2)), + DECIMAL("Decimal", BigDecimal.valueOf(10)), + } + + // -- Inner Type ---------------------------------------------------------- // + + enum class BaseDataUnit { + + BIT, + BYTE, + } + + // -- Inner Type ---------------------------------------------------------- // + + open class DataUnit( + val name: String, + val abbreviation: String, + val isLarge: Boolean, + val baseDataUnit: BaseDataUnit, + val numberSystem: NumberSystem, + private val exponent: Int, + ) { + + fun toBits(value: BigDecimal, mathContext: MathContext): BigDecimal { + val conversionFactor = + when (baseDataUnit) { + BIT -> numberSystem.base.pow(exponent, mathContext) + BYTE -> numberSystem.base.pow(exponent, mathContext).multiply(bigDecimalEight) + } + return value.multiply(conversionFactor, mathContext) + } + + fun fromBits(value: BigDecimal, mathContext: MathContext): BigDecimal { + val conversionFactor = + when (baseDataUnit) { + BIT -> numberSystem.base.pow(exponent, mathContext) + BYTE -> numberSystem.base.pow(exponent, mathContext).multiply(bigDecimalEight) + } + return value.divide(conversionFactor, mathContext) + } + + override fun toString(): String = name + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/MathContextUnitConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/MathContextUnitConverter.kt new file mode 100644 index 00000000..dd111371 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/MathContextUnitConverter.kt @@ -0,0 +1,187 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter + +import com.intellij.openapi.Disposable +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.COLUMNS_MEDIUM +import com.intellij.ui.dsl.builder.COLUMNS_TINY +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.whenItemSelectedFromUi +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer +import dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer.Companion.ALL_AVAILABLE_LOCALES +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindIntTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue +import java.math.BigDecimal +import java.math.MathContext +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.text.ParsePosition +import java.util.Locale + +abstract class MathContextUnitConverter( + configurationKeyPrefix: String, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + title: String, +) : UnitConverter(parentDisposable, title) { + // -- Properties ---------------------------------------------------------- // + + private var parsingLocale = + configuration.register("${configurationKeyPrefix}parsingLocale", DEFAULT_PARSING_LOCALE) + private val roundingMode = + configuration.register("${configurationKeyPrefix}roundingMode", DEFAULT_ROUNDING_MODE) + private val decimalPlaces = + configuration.register("${configurationKeyPrefix}decimalPlaces", DEFAULT_DECIMAL_PLACES) + private val precision = + configuration.register("${configurationKeyPrefix}precision", DEFAULT_PRECISION) + + private val parsingDecimalSeparatorInfo = ValueProperty("") + var mathContext = createMathContext() + private set + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + open fun Panel.buildAdditionalSettingsUi() { + // Override if needed + } + + fun String.parseBigDecimal(): BigDecimal { + val decimalFormat = + DecimalFormat().apply { + decimalFormatSymbols = DecimalFormatSymbols.getInstance(parsingLocale.get().locale) + } + // The `DecimalFormat` is non-strict. So `12foo3` would return `123`. + val parsePosition = ParsePosition(0) + val parsedNumber: Number? = decimalFormat.parse(this, parsePosition) + if (parsePosition.index != this.length || parsedNumber == null) { + throw NumberFormatException("Invalid number format: $this") + } + return BigDecimal(parsedNumber.toString(), mathContext) + } + + fun BigDecimal.toFormatted() = + this.stripTrailingZeros() + .setScale(decimalPlaces.get(), roundingMode.get().javaMathRoundingMode) + .toPlainString() + .let { + // Remove trailing zeros + if (it.contains(".")) { + val trimmed = it.trimEnd('0') + if (trimmed.endsWith('.')) trimmed.dropLast(1) else trimmed + } else { + it + } + } + .replace(".", getDecimalSeparator()) + + override fun sync() { + mathContext = createMathContext() + + val decimalSeparator = getDecimalSeparator() + val postfix = + when (decimalSeparator) { + "." -> " (dot)" + "," -> " (comma)" + else -> "" + } + parsingDecimalSeparatorInfo.set( + "Decimal separator: $decimalSeparator$postfix" + ) + + doSync() + } + + abstract fun doSync() + + // -- Private Methods ----------------------------------------------------- // + + private fun getDecimalSeparator(): String = + DecimalFormatSymbols.getInstance(parsingLocale.get().locale).decimalSeparator.toString() + + @Suppress("UnstableApiUsage") + override fun Panel.buildSettingsUi() { + collapsibleGroup("Settings") { + buildAdditionalSettingsUi() + row { + comboBox(ALL_AVAILABLE_LOCALES) + .label("Locale for parsing:") + .bindItem(parsingLocale) + .columns(COLUMNS_MEDIUM) + .whenItemSelectedFromUi { sync() } + } + .layout(RowLayout.PARENT_GRID) + .bottomGap(BottomGap.NONE) + row { + cell() + label("").bindText(parsingDecimalSeparatorInfo) + } + .layout(RowLayout.PARENT_GRID) + .topGap(TopGap.NONE) + row { + textField() + .label("Decimal places:") + .bindIntTextImproved(decimalPlaces) + .validateLongValue(LongRange(1, 50)) + .columns(COLUMNS_TINY) + .whenTextChangedFromUi { sync() } + } + .layout(RowLayout.PARENT_GRID) + row { + comboBox(RoundingMode.entries) + .label("Rounding mode:") + .bindItem(roundingMode) + .whenItemSelectedFromUi { sync() } + } + .layout(RowLayout.PARENT_GRID) + row { + textField() + .label("Precision:") + .bindIntTextImproved(precision) + .validateLongValue(LongRange(1, 100)) + .columns(COLUMNS_TINY) + .whenTextChangedFromUi { sync() } + } + .layout(RowLayout.PARENT_GRID) + } + .topGap(TopGap.NONE) + } + + private fun createMathContext() = + MathContext(precision.get(), roundingMode.get().javaMathRoundingMode) + + // -- Inner Type ---------------------------------------------------------- // + + private enum class RoundingMode( + val title: String, + val javaMathRoundingMode: java.math.RoundingMode, + ) { + + DOWN("Down", java.math.RoundingMode.DOWN), + UP("Up", java.math.RoundingMode.UP), + CEILING("Ceiling", java.math.RoundingMode.CEILING), + FLOOR("Floor", java.math.RoundingMode.FLOOR), + HALF_UP("Half up", java.math.RoundingMode.HALF_UP), + HALF_DOWN("Half down", java.math.RoundingMode.HALF_DOWN), + HALF_EVEN("Half even", java.math.RoundingMode.HALF_EVEN); + + override fun toString(): String = title + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val DEFAULT_ROUNDING_MODE = RoundingMode.HALF_UP + private const val DEFAULT_DECIMAL_PLACES = 5 + private const val DEFAULT_PRECISION = 50 + private val DEFAULT_PARSING_LOCALE = LocaleContainer(Locale.getDefault()) + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/TimeConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/TimeConverter.kt similarity index 53% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/TimeConverter.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/TimeConverter.kt index b70b835f..443bf9a9 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/TimeConverter.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/TimeConverter.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter import com.intellij.openapi.Disposable import com.intellij.openapi.diagnostic.logger @@ -9,24 +9,24 @@ import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.columns import com.intellij.ui.dsl.builder.whenTextChangedFromUi import com.intellij.ui.layout.not -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.PropertyComponentPredicate -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.isWithinLongRange -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateBigDecimalValue -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.CENTURIES -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.DAYS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.DECADES -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.HOURS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.MILLENNIUMS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.MILLISECONDS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.MINUTES -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.MONTHS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.NANOSECONDS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.SECONDS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.WEEKS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.YEARS -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.isWithinLongRange +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.PropertyComponentPredicate +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateBigDecimalValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.CENTURIES +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.DAYS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.DECADES +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.HOURS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.MILLENNIUMS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.MILLISECONDS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.MINUTES +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.MONTHS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.NANOSECONDS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.SECONDS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.WEEKS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.TimeConverter.ChangeOrigin.YEARS import java.math.BigDecimal import java.math.BigDecimal.TEN import java.math.BigDecimal.ZERO @@ -34,13 +34,17 @@ import java.text.DecimalFormat import java.time.Duration import javax.swing.JComponent -internal class TimeConverter( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : MathContextUnitConverter(CONFIGURATION_KEY_PREFIX, configuration, parentDisposable, "Time") { - // -- Properties -------------------------------------------------------------------------------------------------- // +class TimeConverter(configuration: DeveloperToolConfiguration, parentDisposable: Disposable) : + MathContextUnitConverter(CONFIGURATION_KEY_PREFIX, configuration, parentDisposable, "Time") { + // -- Properties ---------------------------------------------------------- // - private val nanoseconds = configuration.register("${CONFIGURATION_KEY_PREFIX}timeNanoseconds", ZERO, INPUT, NANOSECONDS_EXAMPLE) + private val nanoseconds = + configuration.register( + key = "${CONFIGURATION_KEY_PREFIX}timeNanoseconds", + defaultValue = ZERO, + propertyType = INPUT, + example = NANOSECONDS_EXAMPLE, + ) private val nanosecondsFormatted = ValueProperty("0") private val millisecondsFormatted = ValueProperty("0") @@ -58,8 +62,8 @@ internal class TimeConverter( private val hoursDetail = ValueProperty("0") private val daysDetail = ValueProperty("0") - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // @Suppress("UnstableApiUsage") override fun Panel.buildUi() { @@ -68,78 +72,100 @@ internal class TimeConverter( val valueProperty: ValueProperty, val changeOrigin: ChangeOrigin, val contextHelp: String? = null, - val detail: ValueProperty? = null + val detail: ValueProperty? = null, ) listOf( - TimeField("Nanoseconds", nanosecondsFormatted, NANOSECONDS), - TimeField("Milliseconds", millisecondsFormatted, MILLISECONDS), - TimeField("Seconds", secondsFormatted, SECONDS), - TimeField("Minutes", minutesFormatted, MINUTES, null, minutesDetail), - TimeField("Hours", hoursFormatted, HOURS, null, hoursDetail), - TimeField("Days", daysFormatted, DAYS, null, daysDetail), - TimeField("Months", monthsFormatted, MONTHS, "One month is equal to 30.416 days (365/12)."), - TimeField("Years", yearsFormatted, YEARS), - TimeField("Decades", decadesFormatted, DECADES), - TimeField("Centuries", centuriesFormatted, CENTURIES), - TimeField("Millenniums", millenniumsFormatted, MILLENNIUMS), - ).forEach { (title, valueProperty, changeOrigin, contextHelp, detail) -> - row { - lateinit var textField: JBTextField - textField = textField() - .validateBigDecimalValue(ZERO, mathContext) { it.parseBigDecimal() } - .label("$title:") - .bindText(valueProperty) - .whenTextChangedFromUi { convert(changeOrigin, textField) } - .columns(15) - .component - if (detail != null) { - comment("") - .bindText(detail) - .visibleIf(PropertyComponentPredicate(detail, "").not()) - } - if (contextHelp != null) { - contextHelp(contextHelp) - } - }.layout(RowLayout.PARENT_GRID) - } + TimeField("Nanoseconds", nanosecondsFormatted, NANOSECONDS), + TimeField("Milliseconds", millisecondsFormatted, MILLISECONDS), + TimeField("Seconds", secondsFormatted, SECONDS), + TimeField("Minutes", minutesFormatted, MINUTES, null, minutesDetail), + TimeField("Hours", hoursFormatted, HOURS, null, hoursDetail), + TimeField("Days", daysFormatted, DAYS, null, daysDetail), + TimeField("Months", monthsFormatted, MONTHS, "One month is equal to 30.416 days (365/12)."), + TimeField("Years", yearsFormatted, YEARS), + TimeField("Decades", decadesFormatted, DECADES), + TimeField("Centuries", centuriesFormatted, CENTURIES), + TimeField("Millenniums", millenniumsFormatted, MILLENNIUMS), + ) + .forEach { (title, valueProperty, changeOrigin, contextHelp, detail) -> + row { + lateinit var textField: JBTextField + textField = + textField() + .validateBigDecimalValue(ZERO, mathContext) { it.parseBigDecimal() } + .label("$title:") + .bindText(valueProperty) + .whenTextChangedFromUi { convert(changeOrigin, textField) } + .columns(15) + .component + if (detail != null) { + comment("").bindText(detail).visibleIf(PropertyComponentPredicate(detail, "").not()) + } + if (contextHelp != null) { + contextHelp(contextHelp) + } + } + .layout(RowLayout.PARENT_GRID) + } } override fun doSync() { convert(null, null, nanoseconds.get()) } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun convert( changeOrigin: ChangeOrigin? = null, changeOriginComponent: JComponent? = null, - changeOriginAsNanoseconds: BigDecimal? = null + changeOriginAsNanoseconds: BigDecimal? = null, ) { if (changeOriginComponent != null && validate().any { it.component == changeOriginComponent }) { return } try { - val monthToNanoseconds = BigDecimal.valueOf(365L).divide(BigDecimal.valueOf(12L), mathContext).multiply( - DAYS_TO_NANOSECONDS, mathContext) + val monthToNanoseconds = + BigDecimal.valueOf(365L) + .divide(BigDecimal.valueOf(12L), mathContext) + .multiply(DAYS_TO_NANOSECONDS, mathContext) val millenniumsToNanoseconds = CENTURIES_TO_NANOSECONDS.multiply(TEN, mathContext) - val changeOriginAsNanosecondsToUse: BigDecimal? = when (changeOrigin) { - NANOSECONDS -> nanosecondsFormatted.get().parseBigDecimal() - MILLISECONDS -> millisecondsFormatted.get().parseBigDecimal().multiply(MILLISECONDS_TO_NANOSECONDS, mathContext) - SECONDS -> secondsFormatted.get().parseBigDecimal().multiply(SECONDS_TO_NANOSECONDS, mathContext) - MINUTES -> minutesFormatted.get().parseBigDecimal().multiply(MINUTES_TO_NANOSECONDS, mathContext) - HOURS -> hoursFormatted.get().parseBigDecimal().multiply(HOURS_TO_NANOSECONDS, mathContext) - DAYS -> daysFormatted.get().parseBigDecimal().multiply(DAYS_TO_NANOSECONDS, mathContext) - WEEKS -> weeksFormatted.get().parseBigDecimal().multiply(WEEK_TO_NANOSECONDS, mathContext) - MONTHS -> monthsFormatted.get().parseBigDecimal().multiply(monthToNanoseconds, mathContext) - YEARS -> yearsFormatted.get().parseBigDecimal().multiply(YEARS_TO_NANOSECONDS, mathContext) - DECADES -> decadesFormatted.get().parseBigDecimal().multiply(DECADES_TO_NANOSECONDS, mathContext) - CENTURIES -> centuriesFormatted.get().parseBigDecimal().multiply(CENTURIES_TO_NANOSECONDS, mathContext) - MILLENNIUMS -> millenniumsFormatted.get().parseBigDecimal().multiply(millenniumsToNanoseconds, mathContext) - else -> changeOriginAsNanoseconds!! - } + val changeOriginAsNanosecondsToUse: BigDecimal? = + when (changeOrigin) { + NANOSECONDS -> nanosecondsFormatted.get().parseBigDecimal() + MILLISECONDS -> + millisecondsFormatted + .get() + .parseBigDecimal() + .multiply(MILLISECONDS_TO_NANOSECONDS, mathContext) + SECONDS -> + secondsFormatted.get().parseBigDecimal().multiply(SECONDS_TO_NANOSECONDS, mathContext) + MINUTES -> + minutesFormatted.get().parseBigDecimal().multiply(MINUTES_TO_NANOSECONDS, mathContext) + HOURS -> + hoursFormatted.get().parseBigDecimal().multiply(HOURS_TO_NANOSECONDS, mathContext) + DAYS -> daysFormatted.get().parseBigDecimal().multiply(DAYS_TO_NANOSECONDS, mathContext) + WEEKS -> weeksFormatted.get().parseBigDecimal().multiply(WEEK_TO_NANOSECONDS, mathContext) + MONTHS -> + monthsFormatted.get().parseBigDecimal().multiply(monthToNanoseconds, mathContext) + YEARS -> + yearsFormatted.get().parseBigDecimal().multiply(YEARS_TO_NANOSECONDS, mathContext) + DECADES -> + decadesFormatted.get().parseBigDecimal().multiply(DECADES_TO_NANOSECONDS, mathContext) + CENTURIES -> + centuriesFormatted + .get() + .parseBigDecimal() + .multiply(CENTURIES_TO_NANOSECONDS, mathContext) + MILLENNIUMS -> + millenniumsFormatted + .get() + .parseBigDecimal() + .multiply(millenniumsToNanoseconds, mathContext) + else -> changeOriginAsNanoseconds!! + } if (changeOriginAsNanosecondsToUse == null) { return } @@ -153,7 +179,8 @@ internal class TimeConverter( nanosecondsFormatted.set(nanoseconds.get().toFormatted()) } if (changeOrigin != MILLISECONDS) { - val milliseconds = changeOriginAsNanosecondsToUse.divide(MILLISECONDS_TO_NANOSECONDS, mathContext) + val milliseconds = + changeOriginAsNanosecondsToUse.divide(MILLISECONDS_TO_NANOSECONDS, mathContext) millisecondsFormatted.set(milliseconds.toFormatted()) } if (changeOrigin != SECONDS) { @@ -190,7 +217,8 @@ internal class TimeConverter( centuriesFormatted.set(centuries.toFormatted()) } if (changeOrigin != MILLENNIUMS) { - val millenniums = changeOriginAsNanosecondsToUse.divide(millenniumsToNanoseconds, mathContext) + val millenniums = + changeOriginAsNanosecondsToUse.divide(millenniumsToNanoseconds, mathContext) millenniumsFormatted.set(millenniums.toFormatted()) } @@ -203,7 +231,8 @@ internal class TimeConverter( private fun formatDetails(days: BigDecimal, hours: BigDecimal, minutes: BigDecimal) { try { run { - val daysAsNanos = days.multiply(DAYS_TO_NANOSECONDS, mathContext).setScale(0, mathContext.roundingMode) + val daysAsNanos = + days.multiply(DAYS_TO_NANOSECONDS, mathContext).setScale(0, mathContext.roundingMode) if (daysAsNanos.isWithinLongRange()) { val duration = Duration.ofNanos(daysAsNanos.longValueExact()) daysDetail.set( @@ -214,14 +243,14 @@ internal class TimeConverter( append("${DETAILS_FORMAT.format(duration.toSecondsPart())}s") } ) - } - else { + } else { daysDetail.set("") } } run { - val hoursAsNanos = hours.multiply(HOURS_TO_NANOSECONDS, mathContext).setScale(0, mathContext.roundingMode) + val hoursAsNanos = + hours.multiply(HOURS_TO_NANOSECONDS, mathContext).setScale(0, mathContext.roundingMode) if (hoursAsNanos.isWithinLongRange()) { val duration = Duration.ofNanos(hoursAsNanos.longValueExact()) hoursDetail.set( @@ -231,14 +260,16 @@ internal class TimeConverter( append("${DETAILS_FORMAT.format(duration.toSecondsPart())}s") } ) - } - else { + } else { hoursDetail.set("") } } run { - val minutesAsNanos = minutes.multiply(MINUTES_TO_NANOSECONDS, mathContext).setScale(0, mathContext.roundingMode) + val minutesAsNanos = + minutes + .multiply(MINUTES_TO_NANOSECONDS, mathContext) + .setScale(0, mathContext.roundingMode) if (minutesAsNanos.isWithinLongRange()) { val duration = Duration.ofNanos(minutesAsNanos.longValueExact()) minutesDetail.set( @@ -247,8 +278,7 @@ internal class TimeConverter( append("${DETAILS_FORMAT.format(duration.toSecondsPart())}s") } ) - } - else { + } else { minutesDetail.set("") } } @@ -260,7 +290,7 @@ internal class TimeConverter( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class ChangeOrigin { @@ -275,11 +305,11 @@ internal class TimeConverter( YEARS, DECADES, CENTURIES, - MILLENNIUMS + MILLENNIUMS, } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @@ -301,4 +331,4 @@ internal class TimeConverter( private val DECADES_TO_NANOSECONDS = BigDecimal.valueOf(Duration.ofDays(365 * 10).toNanos()) private val CENTURIES_TO_NANOSECONDS = BigDecimal.valueOf(Duration.ofDays(365 * 100).toNanos()) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/TransferRateConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/TransferRateConverter.kt similarity index 58% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/TransferRateConverter.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/TransferRateConverter.kt index 24846794..45958281 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/TransferRateConverter.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/TransferRateConverter.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter import com.intellij.openapi.Disposable import com.intellij.ui.components.JBTextField @@ -13,38 +13,58 @@ import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.whenStateChangedFromUi import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateBigDecimalValue -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.DataUnit -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.NumberSystem -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.bitDataUnit -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.dataUnits -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateBigDecimalValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.DataUnit +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.NumberSystem +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.bitDataUnit +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter.DataUnits.dataUnits import java.math.BigDecimal import java.math.BigDecimal.ONE import java.math.BigDecimal.ZERO import java.util.concurrent.TimeUnit -internal class TransferRateConverter( +class TransferRateConverter( configuration: DeveloperToolConfiguration, parentDisposable: Disposable, -) : MathContextUnitConverter(CONFIGURATION_KEY_PREFIX, configuration, parentDisposable, "Transfer Rate") { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val timeDimension = configuration.register("${CONFIGURATION_KEY_PREFIX}timeDimension", DEFAULT_TIME_DIMENSION) - private val showLargeDataUnits = configuration.register("${CONFIGURATION_KEY_PREFIX}showLargeDataUnits", DEFAULT_SHOW_LARGE_DATA_UNITS) +) : + MathContextUnitConverter( + CONFIGURATION_KEY_PREFIX, + configuration, + parentDisposable, + "Transfer Rate", + ) { + // -- Properties ---------------------------------------------------------- // + + private val timeDimension = + configuration.register("${CONFIGURATION_KEY_PREFIX}timeDimension", DEFAULT_TIME_DIMENSION) + private val showLargeDataUnits = + configuration.register( + "${CONFIGURATION_KEY_PREFIX}showLargeDataUnits", + DEFAULT_SHOW_LARGE_DATA_UNITS, + ) private val useCombinedAbbreviationNotation = - configuration.register("${CONFIGURATION_KEY_PREFIX}useCombinedAbbreviationNotation", DEFAULT_USE_COMBINED_ABBREVIATION_NOTATION) - private val bitTransferRateValue = configuration.register("${CONFIGURATION_KEY_PREFIX}bitTransferRateValue", ZERO, INPUT, DEFAULT_BIT_TRANSFER_RATE_VALUE) + configuration.register( + "${CONFIGURATION_KEY_PREFIX}useCombinedAbbreviationNotation", + DEFAULT_USE_COMBINED_ABBREVIATION_NOTATION, + ) + private val bitTransferRateValue = + configuration.register( + "${CONFIGURATION_KEY_PREFIX}bitTransferRateValue", + ZERO, + INPUT, + DEFAULT_BIT_TRANSFER_RATE_VALUE, + ) private val transferRateProperties: List = createTransferRateProperties() private val bitTransferRateProperty = transferRateProperties.first { it.dataUnit == bitDataUnit } private var lastTimeDimension: TransferRateTimeDimension = timeDimension.get() - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // @Suppress("UnstableApiUsage") override fun Panel.buildUi() { @@ -59,39 +79,45 @@ internal class TransferRateConverter( NumberSystem.entries.forEach { numberSystem -> group(numberSystem.title, true) { - transferRateProperties - .filter { it.dataUnit.numberSystem == numberSystem } - .forEach { transferRateDataUnitProperty -> - row { - val formattedLabel = label("") - .bindText(transferRateDataUnitProperty.inputTitle) - .gap(RightGap.SMALL) - lateinit var formattedFieldTextField: Cell - formattedFieldTextField = textField() - .bindText(transferRateDataUnitProperty.formattedValue) - .validateBigDecimalValue(ZERO, mathContext) { it.parseBigDecimal() } - .resizableColumn() - .align(Align.FILL) - .whenTextChangedFromUi { - convertByInputFieldChange(formattedFieldTextField.component, transferRateDataUnitProperty) + transferRateProperties + .filter { it.dataUnit.numberSystem == numberSystem } + .forEach { transferRateDataUnitProperty -> + row { + val formattedLabel = + label("").bindText(transferRateDataUnitProperty.inputTitle).gap(RightGap.SMALL) + lateinit var formattedFieldTextField: Cell + formattedFieldTextField = + textField() + .bindText(transferRateDataUnitProperty.formattedValue) + .validateBigDecimalValue(ZERO, mathContext) { it.parseBigDecimal() } + .resizableColumn() + .align(Align.FILL) + .whenTextChangedFromUi { + convertByInputFieldChange( + formattedFieldTextField.component, + transferRateDataUnitProperty, + ) + } + + if (transferRateDataUnitProperty.dataUnit.isLarge) { + formattedLabel.visibleIf(showLargeDataUnits) + formattedFieldTextField.visibleIf(showLargeDataUnits) + } } - - if (transferRateDataUnitProperty.dataUnit.isLarge) { - formattedLabel.visibleIf(showLargeDataUnits) - formattedFieldTextField.visibleIf(showLargeDataUnits) - } - }.layout(RowLayout.PARENT_GRID) - } - }.bottomGap(BottomGap.NONE).topGap(TopGap.NONE) + .layout(RowLayout.PARENT_GRID) + } + } + .bottomGap(BottomGap.NONE) + .topGap(TopGap.NONE) } } @Suppress("UnstableApiUsage") override fun Panel.buildAdditionalSettingsUi() { row { - checkBox("Show large data units") - .bindSelected(showLargeDataUnits) - .whenStateChangedFromUi { sync() } + checkBox("Show large data units").bindSelected(showLargeDataUnits).whenStateChangedFromUi { + sync() + } } row { checkBox("Use combined abbreviation notation") @@ -107,16 +133,14 @@ internal class TransferRateConverter( bitTransferRateProperty.formattedValue.set(bitTransferRateValue.get().toFormatted()) convertByInputFieldChange(null, bitTransferRateProperty) - transferRateProperties.forEach { - it.sync() - } + transferRateProperties.forEach { it.sync() } } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun convertByInputFieldChange( inputFieldComponent: JBTextField?, - inputFieldTransferRateProperty: TransferRateProperty + inputFieldTransferRateProperty: TransferRateProperty, ) { if (inputFieldComponent != null && validate().any { it.component == inputFieldComponent }) { return @@ -124,9 +148,10 @@ internal class TransferRateConverter( if (inputFieldTransferRateProperty != bitTransferRateProperty) { val inputValue = inputFieldTransferRateProperty.formattedValue.get().parseBigDecimal() - bitTransferRateValue.set(inputFieldTransferRateProperty.dataUnit.toBits(inputValue, mathContext)) - } - else { + bitTransferRateValue.set( + inputFieldTransferRateProperty.dataUnit.toBits(inputValue, mathContext) + ) + } else { bitTransferRateValue.set(bitTransferRateProperty.formattedValue.get().parseBigDecimal()) } @@ -137,7 +162,7 @@ internal class TransferRateConverter( private fun convertByTimeDimensionChange( originTimeDimension: TransferRateTimeDimension, - targetTimeDimension: TransferRateTimeDimension + targetTimeDimension: TransferRateTimeDimension, ) { // No validation, always use the last value bits value @@ -148,47 +173,49 @@ internal class TransferRateConverter( } } - private fun createTransferRateProperties() = dataUnits.map { - if (it == bitDataUnit) { - TransferRateProperty(it, bitTransferRateValue, bitTransferRateValue.get().toFormatted()) { createTitle(it, timeDimension) } - .apply { formattedValue.set(bitTransferRateValue.get().toFormatted()) } - } - else { - TransferRateProperty(it, null) { createTitle(it, timeDimension) } + private fun createTransferRateProperties() = + dataUnits.map { + if (it == bitDataUnit) { + TransferRateProperty(it, bitTransferRateValue, bitTransferRateValue.get().toFormatted()) { + createTitle(it, timeDimension) + } + .apply { formattedValue.set(bitTransferRateValue.get().toFormatted()) } + } else { + TransferRateProperty(it, null) { createTitle(it, timeDimension) } + } } - } private fun createTitle( dataUnit: DataUnit, - timeDimensionProperty: ValueProperty + timeDimensionProperty: ValueProperty, ): String { val timeDimension = timeDimensionProperty.get() val abbreviationSeparator = if (useCombinedAbbreviationNotation.get()) "p" else "/" return "${dataUnit.name}s per ${timeDimension.shortTitle} (${dataUnit.abbreviation}$abbreviationSeparator${timeDimension.abbreviation})" } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // enum class TransferRateTimeDimension( val title: String, val shortTitle: String, val abbreviation: String, - val seconds: BigDecimal + val seconds: BigDecimal, ) { SECONDS("Seconds", "sec.", "s", ONE), MINUTES("Minutes", "min.", "m", TimeUnit.MINUTES.toSeconds(1).toBigDecimal()), HOURS("Hours", "hours", "h", TimeUnit.HOURS.toSeconds(1).toBigDecimal()), - DAYS("Days", "days", "d", TimeUnit.DAYS.toSeconds(1).toBigDecimal()) + DAYS("Days", "days", "d", TimeUnit.DAYS.toSeconds(1).toBigDecimal()), } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class TransferRateProperty( val dataUnit: DataUnit, private val rawValueReference: ValueProperty? = null, initialFormattedValue: String = "0", - val createTitle: () -> String + val createTitle: () -> String, ) { val formattedValue: ValueProperty = ValueProperty(initialFormattedValue) @@ -198,10 +225,7 @@ internal class TransferRateConverter( inputTitle.set("${createTitle()}:") } - fun setFromBits( - bits: BigDecimal, - unitConverter: MathContextUnitConverter - ) { + fun setFromBits(bits: BigDecimal, unitConverter: MathContextUnitConverter) { val result = dataUnit.fromBits(bits, unitConverter.mathContext) formattedValue.set(with(unitConverter) { result.toFormatted() }) rawValueReference?.set(result) @@ -211,7 +235,7 @@ internal class TransferRateConverter( bits: BigDecimal, originTimeDimension: TransferRateTimeDimension, targetTimeDimension: TransferRateTimeDimension, - unitConverter: MathContextUnitConverter + unitConverter: MathContextUnitConverter, ) { val mathContext = unitConverter.mathContext val timeFactor = originTimeDimension.seconds.divide(targetTimeDimension.seconds, mathContext) @@ -221,7 +245,7 @@ internal class TransferRateConverter( override fun toString(): String = dataUnit.name } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @@ -231,4 +255,4 @@ internal class TransferRateConverter( private val DEFAULT_BIT_TRANSFER_RATE_VALUE = BigDecimal.valueOf(1073741824) private val DEFAULT_TIME_DIMENSION = TransferRateTimeDimension.SECONDS } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/UnitConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/UnitConverter.kt similarity index 72% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/UnitConverter.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/UnitConverter.kt index 5269c6dc..3f2cefa2 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/UnitConverter.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/UnitConverter.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter import com.intellij.openapi.Disposable import com.intellij.openapi.ui.DialogPanel @@ -8,16 +8,13 @@ import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.panel import javax.swing.JComponent -abstract class UnitConverter( - private val parentDisposable: Disposable, - val title: String -) { - // -- Properties -------------------------------------------------------------------------------------------------- // +abstract class UnitConverter(private val parentDisposable: Disposable, val title: String) { + // -- Properties ---------------------------------------------------------- // private lateinit var component: DialogPanel - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun createComponent(): JComponent { component = panel { @@ -53,7 +50,7 @@ abstract class UnitConverter( // Override if needed } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/UnitsConverter.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/UnitsConverter.kt new file mode 100644 index 00000000..4e0f7130 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/converter/unitconverter/UnitsConverter.kt @@ -0,0 +1,147 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.unitconverter + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.ui.TabbedPaneWrapper +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.util.ui.JBUI.Borders +import com.intellij.util.ui.components.BorderLayoutPanel +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.ResetListener +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ScrollPaneBuilder +import javax.swing.ScrollPaneConstants +import javax.swing.event.ChangeEvent +import javax.swing.event.ChangeListener + +class UnitsConverter( + private val configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, +) : DeveloperUiTool(parentDisposable), ResetListener, ChangeListener { + // -- Properties ---------------------------------------------------------- // + + private var lastSelectedUnitConverterIndex by + configuration.register( + "lastSelectedUnitConverterIndex", + DEFAULT_LAST_SELECTED_UNIT_CONVERTER_INDEX, + ) + + private val unitConverters: List = + listOf( + TimeConverter(configuration, parentDisposable), + DataSizeConverter(configuration, parentDisposable), + TransferRateConverter(configuration, parentDisposable), + BaseConverter(configuration, parentDisposable), + ) + private var selectedUnitConverter: UnitConverter = unitConverters[0] + + private lateinit var unitConvertersTabbedPanel: TabbedPaneWrapper + + // -- Initialization ------------------------------------------------------ // + + init { + wrapComponentInScrollPane = false + } + + // -- Exported Methods ---------------------------------------------------- // + + override fun Panel.buildUi() { + unitConvertersTabbedPanel = + TabbedPaneWrapper(parentDisposable).apply { + val tabBorder = Borders.emptyTop(12) + unitConverters.forEach { converter -> + val component = converter.createComponent().apply { border = tabBorder } + addTab( + converter.title, + ScrollPaneBuilder(component) + .horizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER) + .build(), + ) + } + + val initialSelectedUnitConverterIndex = + if (lastSelectedUnitConverterIndex < this.tabCount) { + lastSelectedUnitConverterIndex + } else { + DEFAULT_LAST_SELECTED_UNIT_CONVERTER_INDEX + } + this.selectedIndex = initialSelectedUnitConverterIndex + } + row { + cell(BorderLayoutPanel().apply { addToCenter(unitConvertersTabbedPanel.component) }) + .resizableColumn() + .align(Align.FILL) + } + .bottomGap(BottomGap.SMALL) + .resizableRow() + } + + override fun activated() { + selectedUnitConverter.activate() + + configuration.addResetListener(parentDisposable, this) + + unitConvertersTabbedPanel.addChangeListener(this) + } + + override fun deactivated() { + selectedUnitConverter.deactivate() + + configuration.removeResetListener(this) + + unitConvertersTabbedPanel.removeChangeListener(this) + } + + override fun stateChanged(e: ChangeEvent?) { + val oldSelectedUnitConverter = selectedUnitConverter + val newSelectedUnitConverter = unitConverters[unitConvertersTabbedPanel.selectedIndex] + if (oldSelectedUnitConverter != newSelectedUnitConverter) { + oldSelectedUnitConverter.deactivate() + newSelectedUnitConverter.activate() + selectedUnitConverter = newSelectedUnitConverter + lastSelectedUnitConverterIndex = unitConverters.indexOf(newSelectedUnitConverter) + } + } + + override fun configurationReset() { + sync() + } + + override fun afterBuildUi() { + sync() + } + + // -- Private Methods ----------------------------------------------------- // + + private fun sync() { + unitConverters.forEach { it.sync() } + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Units Converter", contentTitle = "Units Converter") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> UnitsConverter) = { configuration -> + UnitsConverter(configuration, parentDisposable) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val DEFAULT_LAST_SELECTED_UNIT_CONVERTER_INDEX = 0 + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AboutPluginDialog.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AboutPluginDialog.kt new file mode 100644 index 00000000..f3083cb9 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AboutPluginDialog.kt @@ -0,0 +1,99 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.components.JBTabbedPane +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.ui.JBEmptyBorder +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginInfo +import java.awt.Dimension +import javax.swing.Action +import javax.swing.JComponent + +class AboutPluginDialog(project: Project?, parentComponent: JComponent) : + DialogWrapper(project, parentComponent, true, IdeModalityType.IDE) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + + init { + title = "${PluginInfo.pluginName} - About Plugin" + isModal = true + init() + } + + // -- Exported Methods ---------------------------------------------------- // + + override fun createCenterPanel(): JComponent { + val panel = panel { + row { + val tabs = + mapOf("About" to createAboutPluginComponent(), "Changelog" to createChangelogComponent()) + + cell( + JBTabbedPane().apply { + tabs.forEach { (title, component) -> + // Create scroll panes with specific preferred size + val scrollPane = ScrollPaneFactory.createScrollPane(component, true) + scrollPane.preferredSize = Dimension(650, 500) + addTab(title, scrollPane) + } + } + ) + .align(Align.FILL) + } + } + + // Set preferred size on the entire panel + panel.preferredSize = Dimension(650, 500) + return panel + } + + override fun createActions(): Array = arrayOf(myOKAction) + + // -- Private Methods ----------------------------------------------------- // + + private fun createAboutPluginComponent(): JComponent = + panel { + row { text("Thanks for using the ${PluginInfo.pluginName} plugin! â¤") } + .bottomGap(BottomGap.NONE) + row { text("Version: ${PluginInfo.pluginVersion}") }.bottomGap(BottomGap.MEDIUM) + + row { + text( + "Spotted a bug or thought of a new feature? Please create an issue on GitHub." + ) + .gap(RightGap.SMALL) + } + .bottomGap(BottomGap.NONE) + row { + comment( + "(The plugin is intended for all JetBrains IDEs and will therefore not be extended with features that target a specific programming language or framework.)" + ) + } + .topGap(TopGap.NONE) + } + .apply { this.border = JBEmptyBorder(12, 0, 0, 0) } + + private fun createChangelogComponent(): JComponent = panel { + row { + text( + AboutPluginDialog::class.java.getResource(CHANGELOG_HTML_FILE)?.readText() + ?: "Couldn't find 'What's New' text" + ) + } + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val CHANGELOG_HTML_FILE = + "/dev/turingcomplete/intellijdevelopertoolsplugin/changelog.html" + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AddOpenMainDialogActionToMainToolbarTask.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AddOpenMainDialogActionToMainToolbarTask.kt new file mode 100644 index 00000000..65752134 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/AddOpenMainDialogActionToMainToolbarTask.kt @@ -0,0 +1,83 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame + +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.Anchor +import com.intellij.openapi.actionSystem.Constraints +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.actionSystem.IdeActions +import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.dialog.OpenMainDialogAction + +class AddOpenMainDialogActionToMainToolbarTask( + private val openMainDialogAction: AnAction, + private val mainToolbarRightActionGroup: DefaultActionGroup?, + private val mainToolbarActionGroup: DefaultActionGroup?, +) : Runnable { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun run() { + if ( + mainToolbarRightActionGroup != null && + !mainToolbarRightActionGroup.containsAction(openMainDialogAction) + ) { + mainToolbarRightActionGroup.add( + openMainDialogAction, + Constraints(Anchor.BEFORE, "SearchEverywhere"), + ) + } + + if ( + mainToolbarActionGroup != null && !mainToolbarActionGroup.containsAction(openMainDialogAction) + ) { + mainToolbarActionGroup.add(openMainDialogAction, Constraints.LAST) + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + /** + * Notes regarding the already added check: + * - The [DefaultActionGroup.add] in [AddOpenMainDialogActionToMainToolbarTask] will not add the + * action if the toolbar does not exist. Each toolbar exists only either in the new or old UI. + * So, the action will only be added to the toolbar of the currently active UI. + * - If the user activity removed the action from the toolbar, the + * [AddOpenMainDialogActionToMainToolbarTask] will still add the action, but it will be hidden + * (this due to the working toolbar action mechanism). Therefore, the + * [DefaultActionGroup.containsAction] returns true even though the action is not visible. + */ + fun createIfAvailable(): AddOpenMainDialogActionToMainToolbarTask? { + val openMainDialogAction = OpenMainDialogAction.getAction() ?: return null + + val mainToolbarRightActionGroup = + ActionManager.getInstance() + .getAction(IdeActions.GROUP_MAIN_TOOLBAR_RIGHT) + ?.safeCastTo() // New UI + val mainToolbarActionGroup = + ActionManager.getInstance() + .getAction(IdeActions.GROUP_MAIN_TOOLBAR) + ?.safeCastTo() // Old UI + if (mainToolbarRightActionGroup == null && mainToolbarActionGroup == null) { + return null + } + if ( + mainToolbarRightActionGroup?.containsAction(openMainDialogAction) == true || + mainToolbarActionGroup?.containsAction(openMainDialogAction) == true + ) { + return null + } + + return AddOpenMainDialogActionToMainToolbarTask( + openMainDialogAction = openMainDialogAction, + mainToolbarRightActionGroup = mainToolbarRightActionGroup, + mainToolbarActionGroup = mainToolbarActionGroup, + ) + } + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ShowDeveloperToolUtils.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/ShowDeveloperToolUtils.kt similarity index 57% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ShowDeveloperToolUtils.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/ShowDeveloperToolUtils.kt index 0e9a0ae9..69580878 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ShowDeveloperToolUtils.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/ShowDeveloperToolUtils.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionUpdateThread @@ -7,39 +7,43 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAwareAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.DeveloperUiToolFactoryEp -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolService -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactoryEp +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolService object ShowDeveloperToolUtils { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // val showDeveloperToolActions: Array by lazy { createShowDeveloperToolsActions() } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun createShowDeveloperToolsActions(): Array { val showDeveloperToolActions = mutableListOf() val application = ApplicationManager.getApplication() - val showInternalTools = DeveloperToolsApplicationSettings.instance.showInternalTools + val showInternalTools = + DeveloperToolsApplicationSettings.generalSettings.showInternalTools.get() DeveloperUiToolFactoryEp.EP_NAME.forEachExtensionSafe { developerToolFactoryEp -> if (developerToolFactoryEp.internalTool && !showInternalTools) { return@forEachExtensionSafe } - val developerUiToolFactory: DeveloperUiToolFactory<*> = developerToolFactoryEp.createInstance(application) - val showToolAction = createShowToolAction( - id = developerToolFactoryEp.id, - presentation = developerUiToolFactory.getDeveloperUiToolPresentation() - ) + val developerUiToolFactory: DeveloperUiToolFactory<*> = + developerToolFactoryEp.createInstance(application) + val showToolAction = + createShowToolAction( + id = developerToolFactoryEp.id, + presentation = developerUiToolFactory.getDeveloperUiToolPresentation(), + ) showDeveloperToolActions.add(showToolAction) - ActionManager.getInstance().registerAction("show-developer-tool-${developerToolFactoryEp.id}", showToolAction) + ActionManager.getInstance() + .registerAction("show-developer-tool-${developerToolFactoryEp.id}", showToolAction) } return showDeveloperToolActions.sortedBy { it.templateText }.toTypedArray() @@ -59,5 +63,5 @@ object ShowDeveloperToolUtils { } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/ShowDeveloperUiToolKeymapExtension.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/ShowDeveloperUiToolKeymapExtension.kt similarity index 61% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/ShowDeveloperUiToolKeymapExtension.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/ShowDeveloperUiToolKeymapExtension.kt index e00adf23..3d311c2c 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/ShowDeveloperUiToolKeymapExtension.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/ShowDeveloperUiToolKeymapExtension.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.keymap.KeymapExtension @@ -8,12 +8,12 @@ import com.intellij.openapi.keymap.impl.ui.ActionsTreeUtil import com.intellij.openapi.project.Project import com.intellij.openapi.util.Condition import com.intellij.ui.IconManager -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ShowDeveloperToolUtils.showDeveloperToolActions +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.ShowDeveloperToolUtils.showDeveloperToolActions class ShowDeveloperUiToolKeymapExtension : KeymapExtension { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun createGroup(filtered: Condition?, project: Project?): KeymapGroup? { val group = KeymapGroupFactory.getInstance().createGroup("Show Developer Tool", icon) @@ -25,9 +25,14 @@ class ShowDeveloperUiToolKeymapExtension : KeymapExtension { return group } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // - private val icon = IconManager.getInstance().getIcon("dev/turingcomplete/intellijdevelopertoolsplugin/icons/action.svg", ShowDeveloperUiToolKeymapExtension::class.java.classLoader) -} \ No newline at end of file + private val icon = + IconManager.getInstance() + .getIcon( + "dev/turingcomplete/intellijdevelopertoolsplugin/icons/action.svg", + ShowDeveloperUiToolKeymapExtension::class.java.classLoader, + ) +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/ContentPanelHandler.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/ContentPanelHandler.kt new file mode 100644 index 00000000..e41992fe --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/ContentPanelHandler.kt @@ -0,0 +1,126 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.content + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.util.ui.components.BorderLayoutPanel +import com.intellij.util.ui.tree.TreeUtil +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolReference +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.ContentNode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.DeveloperToolNode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.GroupNode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.ToolsMenuTree +import javax.swing.JPanel + +open class ContentPanelHandler( + protected val project: Project?, + protected val parentDisposable: Disposable, + settings: DeveloperToolsInstanceSettings, + groupNodeSelectionEnabled: Boolean = true, + prioritizeVerticalLayout: Boolean = false, +) { + // -- Properties ---------------------------------------------------------- // + + private var selectedContentNode = ValueProperty(null) + + val contentPanel = BorderLayoutPanel() + val toolsMenuTree: ToolsMenuTree + + private val cachedGroupsPanels = mutableMapOf() + private val cachedDeveloperToolsPanels = + mutableMapOf() + + // -- Initialization ------------------------------------------------------ // + + init { + // The creation of `ToolsMenuTree` will trigger the initial node selection + toolsMenuTree = + ToolsMenuTree( + project, + parentDisposable, + settings, + groupNodeSelectionEnabled, + prioritizeVerticalLayout, + ) { node, selectionTriggeredBySearch -> + handleContentNodeSelection(node, selectionTriggeredBySearch) + } + } + + // -- Exported Methods ---------------------------------------------------- // + + fun openTool( + context: T, + reference: OpenDeveloperToolReference, + ) { + toolsMenuTree.selectDeveloperTool(reference.id) { + cachedDeveloperToolsPanels[selectedContentNode.get()]?.openTool(context, reference) + } + } + + fun showTool(id: String) { + toolsMenuTree.selectDeveloperTool(id) {} + } + + protected open fun createDeveloperToolContentPanel( + developerToolNode: DeveloperToolNode + ): DeveloperToolContentPanel = DeveloperToolContentPanel(developerToolNode) + + protected open fun handleContentNodeSelection( + new: ContentNode?, + selectionTriggeredBySearch: Boolean, + ) { + val old = selectedContentNode.get() + if (old != new) { + if (old is DeveloperToolNode) { + cachedDeveloperToolsPanels[old]?.deselected() + } + + if (new == null) { + return + } + + when (new) { + is GroupNode -> { + setContentPanel( + cachedGroupsPanels + .getOrPut(new.developerUiToolGroup.id) { + GroupContentPanel(new) { + selectedContentNode.set(it) + TreeUtil.selectNode(toolsMenuTree, it) + } + } + .panel + ) + selectedContentNode.set(new) + } + + is DeveloperToolNode -> { + setContentPanel( + cachedDeveloperToolsPanels + .getOrPut(new) { createDeveloperToolContentPanel(new) } + .also { it.selected() } + ) + selectedContentNode.set(new) + } + + else -> error("Unexpected menu node: ${new::class}") + } + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun setContentPanel(nodePanel: JPanel) { + contentPanel.apply { + removeAll() + addToCenter(nodePanel) + revalidate() + repaint() + } + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/DeveloperToolContentPanel.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/DeveloperToolContentPanel.kt similarity index 54% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/DeveloperToolContentPanel.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/DeveloperToolContentPanel.kt index 7735eb20..ac0513cf 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/DeveloperToolContentPanel.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/DeveloperToolContentPanel.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.content +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.content import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.ActionUpdateThread @@ -20,36 +20,36 @@ import com.intellij.ui.tabs.TabInfo import com.intellij.ui.tabs.TabsListener import com.intellij.util.ui.JBEmptyBorder import com.intellij.util.ui.components.BorderLayoutPanel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.NotBlankInputValidator -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils.dumbAwareAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.castedObject -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolHandler -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolReference -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.DeveloperToolNode -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.DeveloperToolNode.DeveloperToolContainer -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.NotBlankInputValidator +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.dumbAwareAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.castedObject +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolReference +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.DeveloperToolNode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.DeveloperToolNode.DeveloperToolContainer import javax.swing.Icon import javax.swing.JComponent import kotlin.reflect.cast -internal open class DeveloperToolContentPanel( - protected val developerToolNode: DeveloperToolNode -) : BorderLayoutPanel() { - // -- Properties -------------------------------------------------------------------------------------------------- // +open class DeveloperToolContentPanel(protected val developerToolNode: DeveloperToolNode) : + BorderLayoutPanel() { + // -- Properties ---------------------------------------------------------- // private lateinit var tabs: JBTabs - private lateinit var selectedDeveloperToolInstance: ObservableMutableProperty + private lateinit var selectedDeveloperToolInstance: + ObservableMutableProperty - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { addToTop(createTitleBar()) addToCenter(createMainContent()) } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // fun selected() { selectedDeveloperToolInstance.get().instance.activated() @@ -59,11 +59,16 @@ internal open class DeveloperToolContentPanel( selectedDeveloperToolInstance.get().instance.deactivated() } - fun openTool(context: T, reference: OpenDeveloperToolReference) { + fun openTool( + context: T, + reference: OpenDeveloperToolReference, + ) { val developerUiToolInstance = selectedDeveloperToolInstance.get().instance assert(developerUiToolInstance is OpenDeveloperToolHandler<*>) @Suppress("UNCHECKED_CAST") - (developerUiToolInstance as OpenDeveloperToolHandler).applyOpenDeveloperToolContext(reference.contextClass.cast(context)) + (developerUiToolInstance as OpenDeveloperToolHandler).applyOpenDeveloperToolContext( + reference.contextClass.cast(context) + ) } @Suppress("DialogTitleCapitalization") @@ -78,34 +83,36 @@ internal open class DeveloperToolContentPanel( RelativeFont.LARGE.install(this) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun createTitleBar(): JComponent = panel { - row { - val titleComponent = buildTitle() - - val actions = mutableListOf( - dumbAwareAction("Reset") { - selectedDeveloperToolInstance.get().apply { - configuration.reset() - instance.reset() + // -- Private Methods ----------------------------------------------------- // + + private fun createTitleBar(): JComponent = + panel { + row { + val titleComponent = buildTitle() + + val actions = + mutableListOf( + dumbAwareAction("Reset") { + selectedDeveloperToolInstance.get().apply { + configuration.reset() + instance.reset() + } + }, + createNewWorkbenchAction(), + ) + developerToolNode.developerUiToolPresentation.description?.let { description -> + actions.add( + dumbAwareAction("Show Tool Description") { description.show(titleComponent) } + ) + } + actionsButton(actions = actions.toTypedArray(), icon = AllIcons.General.GearPlain) + .align(AlignX.RIGHT) + .resizableColumn() + .gap(RightGap.SMALL) } - }, - createNewWorkbenchAction() - ) - developerToolNode.developerUiToolPresentation.description?.let { description -> - actions.add( - dumbAwareAction("Show Tool Description") { - description.show(titleComponent) - } - ) + .resizableRow() } - actionsButton( - actions = actions.toTypedArray(), - icon = AllIcons.General.GearPlain - ).align(AlignX.RIGHT).resizableColumn().gap(RightGap.SMALL) - }.resizableRow() - }.apply { border = JBEmptyBorder(0, 8, 0, 8) } + .apply { border = JBEmptyBorder(0, 8, 0, 8) } private fun createMainContent(): JComponent { tabs = JBTabsFactory.createTabs(developerToolNode.project, developerToolNode.parentDisposable) @@ -121,37 +128,44 @@ internal open class DeveloperToolContentPanel( private fun syncTabsSelectionVisibility() { tabs.presentation.isHideTabs = - DeveloperToolsApplicationSettings.instance.hideWorkbenchTabsOnSingleTab && tabs.tabCount == 1 + generalSettings.hideWorkbenchTabsOnSingleTab.get() && tabs.tabCount == 1 } - private fun createTabsChangedListener() = object : TabsListener { - override fun selectionChanged(oldSelection: TabInfo?, newSelection: TabInfo?) { - oldSelection?.castedObject()?.instance?.deactivated() + private fun createTabsChangedListener() = + object : TabsListener { + override fun selectionChanged(oldSelection: TabInfo?, newSelection: TabInfo?) { + oldSelection?.castedObject()?.instance?.deactivated() - if (newSelection != null) { - val newDeveloperToolInstance = newSelection.castedObject() - selectedDeveloperToolInstance.set(newDeveloperToolInstance) - newDeveloperToolInstance.instance.activated() + if (newSelection != null) { + val newDeveloperToolInstance = newSelection.castedObject() + selectedDeveloperToolInstance.set(newDeveloperToolInstance) + newDeveloperToolInstance.instance.activated() + } } } - } private fun addWorkbench(developerToolContainer: DeveloperToolContainer) { val developerToolComponent = developerToolContainer.instance.createComponent() - val tabInfo = TabInfo(developerToolComponent).apply { - setText(developerToolContainer.configuration.name) - setObject(developerToolContainer) - - val destroyAction = createDestroyWorkbenchAction(developerToolContainer.instance, this) - setTabLabelActions(DefaultActionGroup(destroyAction), DeveloperToolContentPanel::class.java.name) + val tabInfo = + TabInfo(developerToolComponent).apply { + setText(developerToolContainer.configuration.name) + setObject(developerToolContainer) + + val destroyAction = createDestroyWorkbenchAction(developerToolContainer.instance, this) + setTabLabelActions( + DefaultActionGroup(destroyAction), + DeveloperToolContentPanel::class.java.name, + ) - val newWorkbenchAction = createNewWorkbenchAction() - setTabPaneActions(DefaultActionGroup(newWorkbenchAction)) - } + val newWorkbenchAction = createNewWorkbenchAction() + setTabPaneActions(DefaultActionGroup(newWorkbenchAction)) + } tabs.addTab(tabInfo) tabs.select(tabInfo, false) tabs.setPopupGroup( - DefaultActionGroup(createRenameWorkbenchAction()), DeveloperToolContentPanel::class.java.name, true + DefaultActionGroup(createRenameWorkbenchAction()), + DeveloperToolContentPanel::class.java.name, + true, ) } @@ -161,14 +175,15 @@ internal open class DeveloperToolContentPanel( override fun actionPerformed(e: AnActionEvent) { val (_, developerToolConfiguration) = selectedDeveloperToolInstance.get() - val inputDialog = InputDialog( - developerToolNode.project, - "New name:", - "Rename", - null, - developerToolConfiguration.name, - NotBlankInputValidator() - ) + val inputDialog = + InputDialog( + developerToolNode.project, + "New name:", + "Rename", + null, + developerToolConfiguration.name, + NotBlankInputValidator(), + ) inputDialog.show() inputDialog.inputString?.let { newName -> developerToolConfiguration.name = newName @@ -186,7 +201,7 @@ internal open class DeveloperToolContentPanel( developerToolNode.destroyDeveloperToolInstance(developerUiTool) syncTabsSelectionVisibility() }, - { tabs.tabs.size > 1 } + { tabs.tabs.size > 1 }, ) private fun createNewWorkbenchAction() = @@ -200,7 +215,7 @@ internal open class DeveloperToolContentPanel( override fun getActionUpdateThread() = ActionUpdateThread.BGT } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class DestroyWorkbenchAction( private val removeTab: () -> Unit, @@ -222,11 +237,11 @@ internal open class DeveloperToolContentPanel( } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { private val CLOSE_ICON: Icon = AllIcons.Actions.Close private val CLOSE_HOVERED_ICON: Icon = AllIcons.Actions.CloseHovered } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/GroupContentPanel.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/GroupContentPanel.kt new file mode 100644 index 00000000..bdc7f20a --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/content/GroupContentPanel.kt @@ -0,0 +1,68 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.content + +import com.intellij.openapi.ui.DialogPanel +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.components.ActionLink +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.ui.JBEmptyBorder +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.UIUtil +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.ContentNode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.DeveloperToolNode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.GroupNode +import javax.swing.JPanel +import javax.swing.ScrollPaneConstants +import org.jdesktop.swingx.VerticalLayout + +class GroupContentPanel( + groupNode: GroupNode, + private val onContentNodeSelection: (ContentNode) -> Unit, +) { + // -- Properties ---------------------------------------------------------- // + + val panel: DialogPanel = + panel { + row { + label(groupNode.developerUiToolGroup.detailTitle).applyToComponent { + font = JBFont.label().asBold() + } + bottomGap(BottomGap.NONE) + } + + indent { + row { + val component = createDeveloperToolLinksPanel(groupNode) + val componentWrapper = + ScrollPaneFactory.createScrollPane(component, true).apply { + horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS + verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS + } + cell(componentWrapper).align(Align.FILL) + } + .resizableRow() + } + } + .apply { border = JBEmptyBorder(0, 8, 0, 8) } + + private fun createDeveloperToolLinksPanel(groupNode: GroupNode) = + object : JPanel(VerticalLayout(UIUtil.DEFAULT_VGAP)) { + init { + groupNode.children().asSequence().filterIsInstance(DeveloperToolNode::class.java).forEach { + developerToolNode -> + add( + ActionLink(developerToolNode.developerUiToolPresentation.groupedMenuTitle) { + onContentNodeSelection(developerToolNode) + } + ) + } + } + } + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/OpenSettingsAction.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/OpenSettingsAction.kt similarity index 57% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/OpenSettingsAction.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/OpenSettingsAction.kt index f920e3a9..b39ba5c1 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/OpenSettingsAction.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/OpenSettingsAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance import com.intellij.CommonBundle import com.intellij.icons.AllIcons @@ -10,32 +10,42 @@ import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsConfigurable +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettingsConfigurable -class OpenSettingsAction : DumbAwareAction( - "Open Developer Tools ${CommonBundle.settingsTitle()}", - null, - AllIcons.General.GearPlain -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // +class OpenSettingsAction : + DumbAwareAction( + "Open Developer Tools ${CommonBundle.settingsTitle()}", + null, + AllIcons.General.GearPlain, + ) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun actionPerformed(e: AnActionEvent) { - ShowSettingsUtil.getInstance().showSettingsDialog(e.project, DeveloperToolsConfigurable::class.java) + ShowSettingsUtil.getInstance() + .showSettingsDialog(e.project, GeneralSettingsConfigurable::class.java) } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { fun openSettings(project: Project?) { val openSettingsAction = OpenSettingsAction() val dataContext = DataContext { if (CommonDataKeys.PROJECT.`is`(it)) project else null } - val event = AnActionEvent.createEvent(openSettingsAction, dataContext, null, OpenSettingsAction::class.java.name, ActionUiKind.NONE, null) + val event = + AnActionEvent.createEvent( + openSettingsAction, + dataContext, + null, + OpenSettingsAction::class.java.name, + ActionUiKind.NONE, + null, + ) ActionUtil.invokeAction(openSettingsAction, event, null) } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/MainDialog.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/MainDialog.kt similarity index 59% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/MainDialog.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/MainDialog.kt index c8ddd75f..3d4467f5 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/MainDialog.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/MainDialog.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.dialog +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.dialog import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.service @@ -7,40 +7,34 @@ import com.intellij.openapi.ui.DialogWrapper import com.intellij.ui.JBSplitter import com.intellij.ui.navigation.Place import com.intellij.util.ui.JBEmptyBorder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsDialogSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.content.ContentPanelHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsDialogSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.content.ContentPanelHandler import javax.swing.Action import javax.swing.JComponent /** - * Note regarding modality: - * It seems that, as of IntelliJ 2023.1 and at least on macOS, a non-modal - * dialog is always shown in front of the IDE window. This can also be seen - * in IntelliJ's "UI DSL Showcase" dialog. + * Note regarding modality: It seems that, as of IntelliJ 2023.1 and at least on macOS, a non-modal + * dialog is always shown in front of the IDE window. This can also be seen in IntelliJ's "UI DSL + * Showcase" dialog. */ -internal class MainDialog( - project: Project? -) : DialogWrapper( - project, - null, - true, - IdeModalityType.MODELESS, - false -), Place.Navigator { - // -- Properties -------------------------------------------------------------------------------------------------- // +class MainDialog(project: Project?) : + DialogWrapper(project, null, true, IdeModalityType.MODELESS, false), Place.Navigator { + // -- Properties ---------------------------------------------------------- // - val contentPanelHandler = ContentPanelHandler(project, disposable, DeveloperToolsDialogSettings.instance) + val contentPanelHandler = + ContentPanelHandler(project, disposable, DeveloperToolsDialogSettings.instance) - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { title = "Developer Tools" setSize(950, 705) - isModal = DeveloperToolsDialogSettings.instance.dialogIsModal + isModal = generalSettings.dialogIsModal.get() init() } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // override fun createCenterPanel(): JBSplitter { return JBSplitter(0.25f).apply { @@ -48,9 +42,8 @@ internal class MainDialog( firstComponent = contentPanelHandler.toolsMenuTree.createWrapperComponent(this@apply) - secondComponent = contentPanelHandler.contentPanel.apply { - border = JBEmptyBorder(0, 0, 0, DIVIDER_WIDTH) - } + secondComponent = + contentPanelHandler.contentPanel.apply { border = JBEmptyBorder(0, 0, 0, DIVIDER_WIDTH) } } } @@ -74,12 +67,12 @@ internal class MainDialog( super.doCancelAction() } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { const val DIVIDER_WIDTH = 4 } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/MainDialogService.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/MainDialogService.kt similarity index 53% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/MainDialogService.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/MainDialogService.kt index 5a52de8d..b67d9d40 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/MainDialogService.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/MainDialogService.kt @@ -1,23 +1,23 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.dialog +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.dialog import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolReference +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolReference import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read import kotlin.concurrent.write @Service(Service.Level.APP) -internal class MainDialogService { - // -- Properties -------------------------------------------------------------------------------------------------- // +class MainDialogService { + // -- Properties ---------------------------------------------------------- // private val dialogLock = ReentrantReadWriteLock() private val dialog = AtomicReference() - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun openDialog(project: Project?) { dialogLock.write { @@ -26,34 +26,31 @@ internal class MainDialogService { val mainDialog = MainDialog(project) this.dialog.set(mainDialog) mainDialog.show() - } - else { + } else { currentDialog.toFront() } } } fun closeDialog() { - dialogLock.write { - dialog.set(null) - } + dialogLock.write { dialog.set(null) } } - fun openTool(project: Project?, context: T, reference: OpenDeveloperToolReference) { + fun openTool( + project: Project?, + context: T, + reference: OpenDeveloperToolReference, + ) { openDialog(project) - dialogLock.read { - dialog.get()?.contentPanelHandler?.openTool(context, reference) - } + dialogLock.read { dialog.get()?.contentPanelHandler?.openTool(context, reference) } } fun showTool(project: Project?, id: String) { openDialog(project) - dialogLock.read { - dialog.get()?.contentPanelHandler?.showTool(id) - } + dialogLock.read { dialog.get()?.contentPanelHandler?.showTool(id) } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/OpenMainDialogAction.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/OpenMainDialogAction.kt similarity index 61% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/OpenMainDialogAction.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/OpenMainDialogAction.kt index 889eaf16..03850a9a 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/dialog/OpenMainDialogAction.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/dialog/OpenMainDialogAction.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.dialog +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.dialog import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionUpdateThread @@ -9,9 +9,9 @@ import com.intellij.openapi.project.DumbAwareAction import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo class OpenMainDialogAction : DumbAwareAction() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun actionPerformed(e: AnActionEvent) { ApplicationManager.getApplication().service().openDialog(e.project) @@ -19,14 +19,16 @@ class OpenMainDialogAction : DumbAwareAction() { override fun getActionUpdateThread() = ActionUpdateThread.BGT - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { - fun getAction(): OpenMainDialogAction? = ActionManager.getInstance().getAction(ID)?.safeCastTo() + fun getAction(): OpenMainDialogAction? = + ActionManager.getInstance().getAction(ID)?.safeCastTo() - private const val ID = "dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.dialog.OpenMainDialogAction" + private const val ID = + "dev.turingcomplete.intellijdevelopertoolsplugin.ui.instance.dialog.OpenMainDialogAction" } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolContext.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolContext.kt new file mode 100644 index 00000000..08406142 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolContext.kt @@ -0,0 +1,10 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling + +interface OpenDeveloperToolContext { + // -- Properties ---------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolHandler.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolHandler.kt similarity index 53% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolHandler.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolHandler.kt index 635fcd1c..32ba0bee 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolHandler.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolHandler.kt @@ -1,13 +1,13 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling interface OpenDeveloperToolHandler { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // fun applyOpenDeveloperToolContext(context: T) - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolReference.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolReference.kt similarity index 59% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolReference.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolReference.kt index e4ff3455..f7e24363 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolReference.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolReference.kt @@ -1,18 +1,18 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling import kotlin.reflect.KClass -internal interface OpenDeveloperToolReference { - // -- Properties -------------------------------------------------------------------------------------------------- // +interface OpenDeveloperToolReference { + // -- Properties ---------------------------------------------------------- // val id: String val contextClass: KClass - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @@ -20,8 +20,9 @@ internal interface OpenDeveloperToolReference { object : OpenDeveloperToolReference { override val id: String get() = id + override val contextClass: KClass get() = contextClass } } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolService.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolService.kt new file mode 100644 index 00000000..9a653f96 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/handling/OpenDeveloperToolService.kt @@ -0,0 +1,51 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.ActionHandlingInstance.TOOL_WINDOW +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.dialog.MainDialogService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.toolwindow.MainToolWindowService + +@Service(Service.Level.PROJECT) +class OpenDeveloperToolService(val project: Project) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun openTool( + context: T, + reference: OpenDeveloperToolReference, + ) { + if (showToolWindow()) { + project.service().openTool(context, reference) + } else { + ApplicationManager.getApplication() + .service() + .openTool(project, context, reference) + } + } + + fun showTool(id: String) { + if (showToolWindow()) { + project.service().showTool(id) + } else { + ApplicationManager.getApplication().service().showTool(project, id) + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun showToolWindow(): Boolean = + if (DeveloperToolsApplicationSettings.generalSettings.autoDetectActionHandlingInstance.get()) { + !DeveloperToolsApplicationSettings.generalSettings.addOpenMainDialogActionToMainToolbar.get() + } else { + DeveloperToolsApplicationSettings.generalSettings.selectedActionHandlingInstance.get() == + TOOL_WINDOW + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/toolwindow/MainToolWindowFactory.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/toolwindow/MainToolWindowFactory.kt new file mode 100644 index 00000000..65534115 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/toolwindow/MainToolWindowFactory.kt @@ -0,0 +1,183 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.toolwindow + +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.intellij.openapi.wm.impl.content.ToolWindowContentUi +import com.intellij.ui.AnimatedIcon +import com.intellij.ui.IconManager +import com.intellij.ui.content.ContentFactory +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.dsl.builder.actionButton +import com.intellij.util.ui.components.BorderLayoutPanel +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsToolWindowSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.content.ContentPanelHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.content.DeveloperToolContentPanel +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.toolwindow.MainToolWindowService.Companion.toolWindowContentPanelHandlerKey +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.ContentNode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu.DeveloperToolNode +import java.awt.Dimension +import javax.swing.JComponent +import javax.swing.JLabel + +class MainToolWindowFactory : ToolWindowFactory, DumbAware { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun init(toolWindow: ToolWindow) { + assert(toolWindow.id == ID) + + toolWindow.component.putClientProperty(ToolWindowContentUi.HIDE_ID_LABEL, "false") + toolWindow.stripeTitle = "Developer Tools" + } + + override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val loaderPanel = + BorderLayoutPanel().apply { addToCenter(JLabel(AnimatedIcon.Default.INSTANCE)) } + toolWindow.contentManager.addContent( + ContentFactory.getInstance().createContent(loaderPanel, "", false) + ) + + ApplicationManager.getApplication().executeOnPooledThread { + // See `DeveloperToolsToolWindowSettings#getInstance` for an explanation + // why the settings must be instantiated on a background thread. + val settings = DeveloperToolsToolWindowSettings.getInstance(project) + + ApplicationManager.getApplication().invokeLater { + toolWindow.contentManager.removeAllContents(true) + + val contentPanelHandler = + ToolWindowContentPanelHandler(settings, project, toolWindow.disposable) + val mainContent = + ContentFactory.getInstance() + .createContent(contentPanelHandler.contentPanel, "", false) + .apply { + preferredFocusableComponent = contentPanelHandler.contentPanel + putUserData(toolWindowContentPanelHandlerKey, contentPanelHandler) + } + toolWindow.contentManager.addContent(mainContent) + toolWindow.contentManager.setSelectedContent(mainContent) + + project.service().setToolWindow(toolWindow) + } + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + private class ToolWindowDeveloperToolContentPanel( + developerToolNode: DeveloperToolNode, + private val toggleMenu: (JComponent) -> Unit, + ) : DeveloperToolContentPanel(developerToolNode) { + + override fun Row.buildTitle(): JComponent { + val menuIcon = + IconManager.getInstance() + .getIcon( + "dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu.svg", + MainToolWindowFactory::class.java.classLoader, + ) + lateinit var toggleMenuActionLink: JComponent + val toggleMenuAction: DumbAwareAction = + object : DumbAwareAction("Show Developer Tool", null, menuIcon) { + + override fun actionPerformed(e: AnActionEvent) { + toggleMenu(toggleMenuActionLink) + } + } + toggleMenuActionLink = actionButton(toggleMenuAction).gap(RightGap.SMALL).component + + @Suppress("DialogTitleCapitalization") + return label(developerToolNode.developerUiToolPresentation.contentTitle) + .applyToComponent { formatTitle() } + .component + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ToolWindowContentPanelHandler( + settings: DeveloperToolsToolWindowSettings, + project: Project, + parentDisposable: Disposable, + ) : + ContentPanelHandler( + project = project, + parentDisposable = parentDisposable, + settings = settings, + groupNodeSelectionEnabled = false, + prioritizeVerticalLayout = true, + ) { + + private var lastToolsMenuTreePopup: JBPopup? = null + private var toolsMenuTreeWrapper: JComponent? + private var lastGeneralSettingsModificationsCounter = generalSettings.modificationsCounter + + init { + toolsMenuTreeWrapper = toolsMenuTree.createWrapperComponent(contentPanel) + Disposer.register(parentDisposable) { toolsMenuTreeWrapper = null } + } + + override fun createDeveloperToolContentPanel( + developerToolNode: DeveloperToolNode + ): DeveloperToolContentPanel = + ToolWindowDeveloperToolContentPanel(developerToolNode, showMenu()) + + override fun handleContentNodeSelection( + new: ContentNode?, + selectionTriggeredBySearch: Boolean, + ) { + super.handleContentNodeSelection(new, selectionTriggeredBySearch) + + if (!selectionTriggeredBySearch) { + lastToolsMenuTreePopup?.takeIf { !it.isDisposed }?.cancel() + } + } + + private fun showMenu(): (JComponent) -> Unit = { menuOwner -> + if (lastGeneralSettingsModificationsCounter != generalSettings.modificationsCounter) { + toolsMenuTree.recreateTreeNodes() + lastGeneralSettingsModificationsCounter = generalSettings.modificationsCounter + } + + lastToolsMenuTreePopup = + JBPopupFactory.getInstance() + .createComponentPopupBuilder(toolsMenuTreeWrapper!!, toolsMenuTree) + .setRequestFocus(true) + .setResizable(true) + .setMovable(true) + .setDimensionServiceKey(project, TOOLS_MENU_TREE_DIMENSION_SERVICE_KEY, false) + .setCancelOnOtherWindowOpen(true) + .setCancelOnClickOutside(true) + .setMinSize(Dimension(220, 200)) + .createPopup() + .apply { + size = Dimension(220, 600) + Disposer.register(parentDisposable, this) + showUnderneathOf(menuOwner) + } + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + const val ID = "Developer Tools" + + private const val TOOLS_MENU_TREE_DIMENSION_SERVICE_KEY = "ToolWindowDeveloperToolContentPanel" + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/toolwindow/MainToolWindowService.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/toolwindow/MainToolWindowService.kt similarity index 66% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/toolwindow/MainToolWindowService.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/toolwindow/MainToolWindowService.kt index 640e42fa..073d8fce 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/toolwindow/MainToolWindowService.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/instance/toolwindow/MainToolWindowService.kt @@ -1,27 +1,27 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.toolwindow +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.toolwindow import com.intellij.openapi.components.Service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowManager -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.content.ContentPanelHandler -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolReference +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.content.ContentPanelHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolReference import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.read import kotlin.concurrent.write @Service(Service.Level.PROJECT) -internal class MainToolWindowService(val project: Project) { - // -- Properties -------------------------------------------------------------------------------------------------- // +class MainToolWindowService(val project: Project) { + // -- Properties ---------------------------------------------------------- // private val toolWindowLock = ReentrantReadWriteLock() private var toolWindow: ToolWindow? = null private var deferredToolTask: ToolTask? = null // Also only used under `toolWindowLock` - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun setToolWindow(toolWindow: ToolWindow) { toolWindowLock.write { @@ -36,13 +36,15 @@ internal class MainToolWindowService(val project: Project) { } } - fun openTool(context: T, reference: OpenDeveloperToolReference) { + fun openTool( + context: T, + reference: OpenDeveloperToolReference, + ) { toolWindowLock.read { if (toolWindow != null) { toolWindow!!.show() doOpenTool(toolWindow!!, context, reference) - } - else { + } else { deferredToolTask = OpenToolTask(context, reference) // The content of the tool window will be added asynchronous. So, the // return of `show()` (or its callback equivalents) will not indicate @@ -58,54 +60,53 @@ internal class MainToolWindowService(val project: Project) { if (toolWindow != null) { toolWindow!!.show() doShowTool(toolWindow!!, id) - } - else { + } else { deferredToolTask = ShowToolTask(id) ToolWindowManager.getInstance(project).getToolWindow(MainToolWindowFactory.ID)?.show() } } } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun doOpenTool( toolWindow: ToolWindow, context: T, - reference: OpenDeveloperToolReference + reference: OpenDeveloperToolReference, ) { - toolWindow.contentManager.getContent(0) + toolWindow.contentManager + .getContent(0) ?.getUserData(toolWindowContentPanelHandlerKey) ?.openTool(context, reference) } - private fun doShowTool( - toolWindow: ToolWindow, - id: String - ) { - toolWindow.contentManager.getContent(0) + private fun doShowTool(toolWindow: ToolWindow, id: String) { + toolWindow.contentManager + .getContent(0) ?.getUserData(toolWindowContentPanelHandlerKey) ?.showTool(id) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private sealed interface ToolTask - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class OpenToolTask( val context: T, - val reference: OpenDeveloperToolReference + val reference: OpenDeveloperToolReference, ) : ToolTask - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class ShowToolTask(val id: String) : ToolTask - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { - val toolWindowContentPanelHandlerKey = Key(ContentPanelHandler::class.simpleName!!) + val toolWindowContentPanelHandlerKey = + Key(ContentPanelHandler::class.simpleName!!) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ContentNode.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ContentNode.kt similarity index 58% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ContentNode.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ContentNode.kt index 4bc482f7..5a789883 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ContentNode.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ContentNode.kt @@ -1,19 +1,19 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu import javax.swing.Icon import javax.swing.tree.DefaultMutableTreeNode -internal sealed class ContentNode( +sealed class ContentNode( val id: String, val title: String, val toolTipText: String? = null, val icon: Icon? = null, val isSecondaryNode: Boolean = false, ) : DefaultMutableTreeNode(title) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/DeveloperToolNode.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/DeveloperToolNode.kt similarity index 50% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/DeveloperToolNode.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/DeveloperToolNode.kt index 38013657..b9d50950 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/DeveloperToolNode.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/DeveloperToolNode.kt @@ -1,36 +1,41 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsInstanceSettings -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation -internal class DeveloperToolNode( +class DeveloperToolNode( private val developerToolId: String, val parentDisposable: Disposable, val project: Project?, val settings: DeveloperToolsInstanceSettings, val developerUiToolPresentation: DeveloperUiToolPresentation, showGrouped: Boolean, - private val developerUiToolCreator: (DeveloperToolConfiguration) -> DeveloperUiTool -) : ContentNode( - id = developerToolId, - title = if (showGrouped) developerUiToolPresentation.groupedMenuTitle else developerUiToolPresentation.menuTitle, - toolTipText = developerUiToolPresentation.contentTitle -) { - // -- Properties -------------------------------------------------------------------------------------------------- // + private val developerUiToolCreator: (DeveloperToolConfiguration) -> DeveloperUiTool, +) : + ContentNode( + id = developerToolId, + title = + if (showGrouped) developerUiToolPresentation.groupedMenuTitle + else developerUiToolPresentation.menuTitle, + toolTipText = developerUiToolPresentation.contentTitle, + ) { + // -- Properties ---------------------------------------------------------- // private val _developerTools: MutableList by lazy { - restoreDeveloperToolInstances().ifEmpty { listOf(doCreateNewDeveloperToolInstance()) }.toMutableList() + restoreDeveloperToolInstances() + .ifEmpty { listOf(doCreateNewDeveloperToolInstance()) } + .toMutableList() } val developerTools: List get() = _developerTools - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // fun createNewDeveloperToolInstance(): DeveloperToolContainer { val developerToolContainer = doCreateNewDeveloperToolInstance() @@ -39,23 +44,27 @@ internal class DeveloperToolNode( } fun destroyDeveloperToolInstance(developerUiTool: DeveloperUiTool) { - _developerTools.find { it.instance === developerUiTool }?.let { - settings.removeDeveloperToolConfiguration( - developerToolId = developerToolId, - developerToolConfiguration = it.configuration - ) - Disposer.dispose(developerUiTool) - } + _developerTools + .find { it.instance === developerUiTool } + ?.let { + settings.removeDeveloperToolConfiguration( + developerToolId = developerToolId, + developerToolConfiguration = it.configuration, + ) + Disposer.dispose(developerUiTool) + } } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun restoreDeveloperToolInstances(): List = - settings.getDeveloperToolConfigurations(developerToolId) - .map { developerToolConfiguration -> doCreateNewDeveloperToolInstance(developerToolConfiguration) } + settings.getDeveloperToolConfigurations(developerToolId).map { developerToolConfiguration -> + doCreateNewDeveloperToolInstance(developerToolConfiguration) + } private fun doCreateNewDeveloperToolInstance( - developerToolConfiguration: DeveloperToolConfiguration = settings.createDeveloperToolConfiguration(developerToolId) + developerToolConfiguration: DeveloperToolConfiguration = + settings.createDeveloperToolConfiguration(developerToolId) ): DeveloperToolContainer { val developerTool = developerUiToolCreator(developerToolConfiguration) developerToolConfiguration.wasConsumedByDeveloperTool = true @@ -63,12 +72,12 @@ internal class DeveloperToolNode( return DeveloperToolContainer(developerTool, developerToolConfiguration) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // data class DeveloperToolContainer( val instance: DeveloperUiTool, - val configuration: DeveloperToolConfiguration + val configuration: DeveloperToolConfiguration, ) - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ExternalNode.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ExternalNode.kt similarity index 54% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ExternalNode.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ExternalNode.kt index 5faeb4af..201ec49f 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ExternalNode.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ExternalNode.kt @@ -1,14 +1,14 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu import com.intellij.openapi.project.Project interface ExternalNode { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // fun selected(project: Project?) - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/GroupNode.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/GroupNode.kt new file mode 100644 index 00000000..ded43f4b --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/GroupNode.kt @@ -0,0 +1,13 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu + +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolGroup + +class GroupNode(val developerUiToolGroup: DeveloperUiToolGroup) : + ContentNode(id = developerUiToolGroup.id, title = developerUiToolGroup.menuTitle) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/RootNode.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/RootNode.kt new file mode 100644 index 00000000..92027a17 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/RootNode.kt @@ -0,0 +1,10 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu + +class RootNode : ContentNode(id = "root", title = "Root") { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ToolsMenuTree.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ToolsMenuTree.kt similarity index 53% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ToolsMenuTree.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ToolsMenuTree.kt index eceadbdb..9fea337f 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/ToolsMenuTree.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/frame/menu/ToolsMenuTree.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.menu import com.intellij.CommonBundle import com.intellij.icons.AllIcons @@ -25,16 +25,16 @@ import com.intellij.util.ui.JBUI.Borders import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel import com.intellij.util.ui.tree.TreeUtil -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.DeveloperUiToolFactoryEp -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsInstanceSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.ChangelogDialog -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.OpenSettingsAction import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactoryEp +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolGroup +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.AboutPluginDialog +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.OpenSettingsAction import javax.swing.JComponent import javax.swing.JPanel import javax.swing.JTree @@ -48,7 +48,7 @@ import javax.swing.tree.DefaultTreeModel import javax.swing.tree.TreePath import javax.swing.tree.TreeSelectionModel -internal class ToolsMenuTree( +class ToolsMenuTree( private val project: Project?, private val parentDisposable: Disposable, private val settings: DeveloperToolsInstanceSettings, @@ -56,17 +56,18 @@ internal class ToolsMenuTree( private val prioritizeVerticalLayout: Boolean = false, private val selectContentNode: (ContentNode, Boolean) -> Unit, ) : SimpleTree() { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private var selectionTriggeredBySearch = false - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { - val (rootNode, defaultGroupNodesToExpand, preferredSelectedDeveloperToolNode) = createTreeNodes() + val (rootNode, defaultGroupNodesToExpand, preferredSelectedDeveloperToolNode) = + createTreeNodes() model = DefaultTreeModel(rootNode) - setCellRenderer(MenuTreeNodeRenderer(DeveloperToolsApplicationSettings.instance.toolsMenuTreeShowGroupNodes)) + setCellRenderer(MenuTreeNodeRenderer(generalSettings.toolsMenuTreeShowGroupNodes.get())) setTreeBorder() putClientProperty(RenderingUtil.ALWAYS_PAINT_SELECTION_AS_FOCUSED, true) background = UIUtil.SIDE_PANEL_BACKGROUND @@ -84,14 +85,14 @@ internal class ToolsMenuTree( selectInitiallySelectedDeveloperToolNode(preferredSelectedDeveloperToolNode) } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // fun recreateTreeNodes() { val (newRootNode, defaultGroupNodesToExpand, _) = createTreeNodes() (model as DefaultTreeModel).setRoot(newRootNode) expandGroupNodes(defaultGroupNodesToExpand) - setCellRenderer(MenuTreeNodeRenderer(DeveloperToolsApplicationSettings.instance.toolsMenuTreeShowGroupNodes)) + setCellRenderer(MenuTreeNodeRenderer(generalSettings.toolsMenuTreeShowGroupNodes.get())) setTreeBorder() revalidate() @@ -109,22 +110,33 @@ internal class ToolsMenuTree( } fun createWrapperComponent(parentComponent: JComponent): JComponent { - val wrapper = BorderLayoutPanel().apply { - border = Borders.empty() - addToCenter(this@ToolsMenuTree) - addToBottom(BorderLayoutPanel().apply { - border = Borders.empty(UIUtil.PANEL_REGULAR_INSETS) - background = UIUtil.SIDE_PANEL_BACKGROUND - val linksPanel = JPanel(VerticalLayout(UIUtil.DEFAULT_VGAP * 2)).apply { - background = UIUtil.SIDE_PANEL_BACKGROUND - add(createSettingsLink()) - add(createWhatsNewLink(parentComponent)) - } - addToCenter(linksPanel) - }) - } + val wrapper = + BorderLayoutPanel().apply { + border = Borders.empty() + addToCenter(this@ToolsMenuTree) + addToBottom( + BorderLayoutPanel().apply { + border = Borders.empty(UIUtil.PANEL_REGULAR_INSETS) + background = UIUtil.SIDE_PANEL_BACKGROUND + val linksPanel = + JPanel(VerticalLayout(UIUtil.DEFAULT_VGAP * 2)).apply { + background = UIUtil.SIDE_PANEL_BACKGROUND + add(createSettingsLink()) + add(createWhatsNewLink(parentComponent)) + } + addToCenter(linksPanel) + } + ) + } return ScrollPaneFactory.createScrollPane(wrapper, true).apply { - border = ColoredSideBorder(null, null, null, JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground(), 1) + border = + ColoredSideBorder( + null, + null, + null, + JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground(), + 1, + ) background = UIUtil.SIDE_PANEL_BACKGROUND viewport.background = UIUtil.SIDE_PANEL_BACKGROUND verticalScrollBar.background = UIUtil.SIDE_PANEL_BACKGROUND @@ -134,68 +146,72 @@ internal class ToolsMenuTree( fun selectDeveloperTool(developerToolId: String, onSuccess: () -> Unit) { TreeUtil.promiseVisit(this) { - val lastPathComponent = it.lastPathComponent - if (lastPathComponent is ContentNode && lastPathComponent.id == developerToolId) { - INTERRUPT - } - else { - CONTINUE - } - }.onSuccess { - if (it != null) { - TreeUtil.selectPath(this, TreeUtil.getPathFromRoot(it.lastPathComponent as ContentNode)).doWhenDone { - onSuccess() + val lastPathComponent = it.lastPathComponent + if (lastPathComponent is ContentNode && lastPathComponent.id == developerToolId) { + INTERRUPT + } else { + CONTINUE } } - else { - throw IllegalStateException("Can't find developer tool with ID: $developerToolId") + .onSuccess { + if (it != null) { + TreeUtil.selectPath(this, TreeUtil.getPathFromRoot(it.lastPathComponent as ContentNode)) + .doWhenDone { onSuccess() } + } else { + throw IllegalStateException("Can't find developer tool with ID: $developerToolId") + } } - } } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun setTreeBorder() { - border = if (DeveloperToolsApplicationSettings.instance.toolsMenuTreeShowGroupNodes) { - Borders.emptyLeft(4) - } - else { - Borders.empty() - } + border = + if (generalSettings.toolsMenuTreeShowGroupNodes.get()) { + Borders.emptyLeft(4) + } else { + Borders.empty() + } } /** - * Directly opening the [com.intellij.openapi.options.ShowSettingsUtil] will - * lead to the error `Slow operations are prohibited on EDT` if called in the - * dialog. - * Using [com.intellij.ui.components.ActionLink] will lead to an error related - * to the `project.messageBus`. + * Directly opening the [com.intellij.openapi.options.ShowSettingsUtil] will lead to the error + * `Slow operations are prohibited on EDT` if called in the dialog. Using + * [com.intellij.ui.components.ActionLink] will lead to an error related to the + * `project.messageBus`. */ @Suppress("DialogTitleCapitalization") - private fun createSettingsLink() = HyperlinkLabel(CommonBundle.settingsTitle()).apply { - icon = AllIcons.General.GearPlain - addHyperlinkListener(object : HyperlinkAdapter() { - - override fun hyperlinkActivated(e: HyperlinkEvent) { - OpenSettingsAction.openSettings(project) - } - }) - } - - private fun createWhatsNewLink(parentComponent: JComponent) = HyperlinkLabel("What's new").apply { - icon = AllIcons.Actions.IntentionBulbGrey - addHyperlinkListener(object : HyperlinkAdapter() { + private fun createSettingsLink() = + HyperlinkLabel(CommonBundle.settingsTitle()).apply { + icon = AllIcons.General.GearPlain + addHyperlinkListener( + object : HyperlinkAdapter() { + + override fun hyperlinkActivated(e: HyperlinkEvent) { + OpenSettingsAction.openSettings(project) + } + } + ) + } - override fun hyperlinkActivated(e: HyperlinkEvent) { - ApplicationManager.getApplication().invokeLater { - ChangelogDialog(project, parentComponent).show() + private fun createWhatsNewLink(parentComponent: JComponent) = + HyperlinkLabel("About").apply { + icon = AllIcons.Actions.IntentionBulbGrey + addHyperlinkListener( + object : HyperlinkAdapter() { + + override fun hyperlinkActivated(e: HyperlinkEvent) { + ApplicationManager.getApplication().invokeLater { + AboutPluginDialog(project, parentComponent).show() + } + } } - } - }) - } + ) + } private fun expandGroupNodes(defaultGroupNodesToExpand: List) { - val expandedGroupNodeIds = settings.expandedGroupNodeIds ?: defaultGroupNodesToExpand.map { it.id }.toSet() + val expandedGroupNodeIds = + settings.expandedGroupNodeIds ?: defaultGroupNodesToExpand.map { it.id }.toSet() TreeUtil.promiseVisit(this) { val lastPathComponent = it.lastPathComponent @@ -206,23 +222,27 @@ internal class ToolsMenuTree( } } - private fun selectInitiallySelectedDeveloperToolNode(preferredSelectedDeveloperToolNode: ContentNode?) { + private fun selectInitiallySelectedDeveloperToolNode( + preferredSelectedDeveloperToolNode: ContentNode? + ) { var lastSelectedContentNodeSelected = false settings.lastSelectedContentNodeId.get()?.let { lastSelectedComponentNodeId -> TreeUtil.promiseVisit(this) { - val lastPathComponent = it.lastPathComponent - if (lastPathComponent is ContentNode && lastPathComponent.id == lastSelectedComponentNodeId) { - INTERRUPT - } - else { - CONTINUE + val lastPathComponent = it.lastPathComponent + if ( + lastPathComponent is ContentNode && lastPathComponent.id == lastSelectedComponentNodeId + ) { + INTERRUPT + } else { + CONTINUE + } } - }.onSuccess { - if (it != null) { - TreeUtil.selectNode(this, it.lastPathComponent as ContentNode) - lastSelectedContentNodeSelected = true + .onSuccess { + if (it != null) { + TreeUtil.selectNode(this, it.lastPathComponent as ContentNode) + lastSelectedContentNodeSelected = true + } } - } } if (lastSelectedContentNodeSelected) { return @@ -230,52 +250,56 @@ internal class ToolsMenuTree( if (preferredSelectedDeveloperToolNode != null) { TreeUtil.selectNode(this, preferredSelectedDeveloperToolNode) - } - else { + } else { // Select first `DeveloperToolNode` - TreeUtil.promiseVisit(this) { if (it.lastPathComponent is DeveloperToolNode) INTERRUPT else CONTINUE } - .onSuccess { it?.let { TreeUtil.selectNode(this, it.lastPathComponent as DeveloperToolNode) } } + TreeUtil.promiseVisit(this) { + if (it.lastPathComponent is DeveloperToolNode) INTERRUPT else CONTINUE + } + .onSuccess { + it?.let { TreeUtil.selectNode(this, it.lastPathComponent as DeveloperToolNode) } + } } } private fun createTreeSelectionListener() = TreeSelectionListener { e -> - e.path?.lastPathComponent + e.path + ?.lastPathComponent ?.safeCastTo() ?.takeIf { groupNodeSelectionEnabled || it !is GroupNode } ?.let { if (it is ExternalNode) { it.selected(project) - } - else { + } else { selectContentNode(it, selectionTriggeredBySearch) settings.lastSelectedContentNodeId.set(it.id) } } } - private fun createTreeExpansionListener() = object : TreeExpansionListener { + private fun createTreeExpansionListener() = + object : TreeExpansionListener { - override fun treeExpanded(event: TreeExpansionEvent?) { - saveState(settings) - } + override fun treeExpanded(event: TreeExpansionEvent?) { + saveState(settings) + } - override fun treeCollapsed(event: TreeExpansionEvent?) { - saveState(settings) - } + override fun treeCollapsed(event: TreeExpansionEvent?) { + saveState(settings) + } - fun saveState(settings: DeveloperToolsInstanceSettings) { - val expandedGroupNodeIds = TreeUtil.collectExpandedPaths(this@ToolsMenuTree) - .map { it.lastPathComponent } - .filterIsInstance() - .map { it.id } - .toSet() - settings.setExpandedGroupNodeIds(expandedGroupNodeIds) + fun saveState(settings: DeveloperToolsInstanceSettings) { + val expandedGroupNodeIds = + TreeUtil.collectExpandedPaths(this@ToolsMenuTree) + .map { it.lastPathComponent } + .filterIsInstance() + .map { it.id } + .toSet() + settings.setExpandedGroupNodeIds(expandedGroupNodeIds) + } } - } private fun createTreeNodes(): Triple, ContentNode?> { - val applicationSettings = DeveloperToolsApplicationSettings.instance - val toolsMenuTreeShowGroupNodes = applicationSettings.toolsMenuTreeShowGroupNodes + val toolsMenuTreeShowGroupNodes = generalSettings.toolsMenuTreeShowGroupNodes.get() val rootNode = RootNode() @@ -294,80 +318,93 @@ internal class ToolsMenuTree( var preferredSelectedDeveloperToolNode: DeveloperToolNode? = null val application = ApplicationManager.getApplication() - val showInternalTools = DeveloperToolsApplicationSettings.instance.showInternalTools + val showInternalTools = generalSettings.showInternalTools.get() DeveloperUiToolFactoryEp.EP_NAME.forEachExtensionSafe { developerToolFactoryEp -> if (developerToolFactoryEp.internalTool && !showInternalTools) { return@forEachExtensionSafe } - val developerUiToolFactory: DeveloperUiToolFactory<*> = developerToolFactoryEp.createInstance(application) + val developerUiToolFactory: DeveloperUiToolFactory<*> = + developerToolFactoryEp.createInstance(application) val context = DeveloperUiToolContext(developerToolFactoryEp.id, prioritizeVerticalLayout) - developerUiToolFactory.getDeveloperUiToolCreator(project, parentDisposable, context)?.let { developerToolCreator -> - val groupId: String? = if (toolsMenuTreeShowGroupNodes) developerToolFactoryEp.groupId else null - val parentNode = if (groupId != null) (groupNodes[groupId] ?: error("Unknown group: $groupId")) else rootNode - val developerToolNode = DeveloperToolNode( - developerToolId = developerToolFactoryEp.id, - project = project, - settings = settings, - parentDisposable = parentDisposable, - developerUiToolPresentation = developerUiToolFactory.getDeveloperUiToolPresentation(), - showGrouped = toolsMenuTreeShowGroupNodes, - developerUiToolCreator = developerToolCreator - ) + developerUiToolFactory.getDeveloperUiToolCreator(project, parentDisposable, context)?.let { + developerToolCreator -> + val groupId: String? = + if (toolsMenuTreeShowGroupNodes) developerToolFactoryEp.groupId else null + val parentNode = + if (groupId != null) (groupNodes[groupId] ?: error("Unknown group: $groupId")) + else rootNode + val developerToolNode = + DeveloperToolNode( + developerToolId = developerToolFactoryEp.id, + project = project, + settings = settings, + parentDisposable = parentDisposable, + developerUiToolPresentation = developerUiToolFactory.getDeveloperUiToolPresentation(), + showGrouped = toolsMenuTreeShowGroupNodes, + developerUiToolCreator = developerToolCreator, + ) parentNode.add(developerToolNode) if (developerToolFactoryEp.preferredSelected) { - check(preferredSelectedDeveloperToolNode == null) { "Multiple initial selected developer tools" } + check(preferredSelectedDeveloperToolNode == null) { + "Multiple initial selected developer tools" + } preferredSelectedDeveloperToolNode = developerToolNode } } } - if (applicationSettings.toolsMenuTreeOrderAlphabetically) { + if (generalSettings.toolsMenuTreeOrderAlphabetically.get()) { TreeUtil.sortChildren(rootNode) { o1, o2 -> o1.title.compareTo(o2.title) } } return Triple(rootNode, defaultGroupNodesToExpand, preferredSelectedDeveloperToolNode) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class MenuTreeSearch(tree: Tree, private val setSelectionTriggeredBySearch: (Boolean) -> Unit) - : TreeSpeedSearch(tree, null as Void?) { + private class MenuTreeSearch( + tree: Tree, + private val setSelectionTriggeredBySearch: (Boolean) -> Unit, + ) : TreeSpeedSearch(tree, null as Void?) { - override fun getElementText(element: Any?): String = (element as TreePath).lastPathComponent.toString() + override fun getElementText(element: Any?): String = + (element as TreePath).lastPathComponent.toString() override fun selectElement(element: Any?, selectedText: String?) { try { setSelectionTriggeredBySearch(true) super.selectElement(element, selectedText) - } - finally { + } finally { setSelectionTriggeredBySearch(false) } } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class MenuTreeNodeRenderer(private val toolsMenuTreeShowGroupNodes: Boolean) : NodeRenderer() { + private class MenuTreeNodeRenderer(private val toolsMenuTreeShowGroupNodes: Boolean) : + NodeRenderer() { - override fun customizeCellRenderer(tree: JTree, - value: Any, - selected: Boolean, - expanded: Boolean, - leaf: Boolean, - row: Int, - hasFocus: Boolean) { + override fun customizeCellRenderer( + tree: JTree, + value: Any, + selected: Boolean, + expanded: Boolean, + leaf: Boolean, + row: Int, + hasFocus: Boolean, + ) { val contentNode = value.uncheckedCastTo(ContentNode::class) val isTopLevelNode = contentNode.parent is RootNode - val textAttributes = if (toolsMenuTreeShowGroupNodes && isTopLevelNode && !contentNode.isSecondaryNode) { - REGULAR_BOLD_ATTRIBUTES - } - else { - REGULAR_ATTRIBUTES - } + val textAttributes = + if (toolsMenuTreeShowGroupNodes && isTopLevelNode && !contentNode.isSecondaryNode) { + REGULAR_BOLD_ATTRIBUTES + } else { + REGULAR_ATTRIBUTES + } append(contentNode.title, textAttributes) icon = contentNode.icon isIconOnTheRight = false @@ -375,5 +412,5 @@ internal class ToolsMenuTree( } } - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/BarcodeGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/BarcodeGenerator.kt similarity index 50% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/BarcodeGenerator.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/BarcodeGenerator.kt index 887539a6..4fa1dcce 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/BarcodeGenerator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/BarcodeGenerator.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType @@ -47,41 +47,42 @@ import com.intellij.ui.layout.ComponentPredicate import com.intellij.ui.layout.not import com.intellij.util.Alarm import com.intellij.util.ui.JBUI -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValidateMinIntValueSide -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bindIntTextImproved -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.toJBColor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateLongValue -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateMinMaxValueRelation -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator.ErrorCorrectionSupport.LEVEL_BITS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator.ErrorCorrectionSupport.LEVEL_ENUM_NAME -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator.ErrorCorrectionSupport.UNSUPPORTED -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ValidateMinIntValueSide +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindIntTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.toJBColor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateMinMaxValueRelation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.BarcodeGenerator.ErrorCorrectionSupport.LEVEL_BITS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.BarcodeGenerator.ErrorCorrectionSupport.LEVEL_ENUM_NAME +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.BarcodeGenerator.ErrorCorrectionSupport.UNSUPPORTED import java.awt.Container import java.awt.Graphics import java.lang.Integer.toHexString import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.util.* +import java.util.Locale import javax.imageio.ImageIO import javax.swing.JButton import javax.swing.JPanel import javax.swing.border.LineBorder -internal class BarcodeGenerator private constructor( +class BarcodeGenerator +private constructor( private val formats: Map, private val context: DeveloperUiToolContext, private val configuration: DeveloperToolConfiguration, private val project: Project?, - parentDisposable: Disposable + parentDisposable: Disposable, ) : DeveloperUiTool(parentDisposable) { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private var liveGeneration = configuration.register("liveGeneration", true) private val format = configuration.register("format", DEFAULT_FORMAT) @@ -94,32 +95,32 @@ internal class BarcodeGenerator private constructor( private val drawPanel by lazy { DrawPanel(configuration) } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // @Suppress("UnstableApiUsage") override fun Panel.buildUi() { lateinit var formatComboBox: ComboBox row { - formatComboBox = comboBox(Format.entries) - .label("Format:") - .bindItem(format) - .whenItemSelectedFromUi { generate() } - .component + formatComboBox = + comboBox(Format.entries) + .label("Format:") + .bindItem(format) + .whenItemSelectedFromUi { generate() } + .component } row { - cell(contentEditor.component) - .validationOnApply(contentEditor.bindValidator(contentErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .align(Align.FILL) - }.layout(RowLayout.INDEPENDENT) + cell(contentEditor.component) + .validationOnApply(contentEditor.bindValidator(contentErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + } + .layout(RowLayout.INDEPENDENT) formats.forEach { (format, configuration) -> with(configuration) { - buildConfigurationUi(ComboBoxPredicate(formatComboBox) { it == format }) { - generate() - } + buildConfigurationUi(ComboBoxPredicate(formatComboBox) { it == format }) { generate() } } } @@ -127,54 +128,77 @@ internal class BarcodeGenerator private constructor( label("Background color:").gap(RightGap.SMALL) cell(ColorPanel(drawPanel.backgroundColor)).gap(RightGap.SMALL) lateinit var backgroundColorButton: JButton - backgroundColorButton = button("Change") { - ColorChooserService.instance.showDialog(project, backgroundColorButton, "Select Background Color", drawPanel.backgroundColor.get())?.let { - drawPanel.backgroundColor.set(it.toJBColor()) - generate() - } - }.component + backgroundColorButton = + button("Change") { + ColorChooserService.instance + .showDialog( + project, + backgroundColorButton, + "Select Background Color", + drawPanel.backgroundColor.get(), + ) + ?.let { + drawPanel.backgroundColor.set(it.toJBColor()) + generate() + } + } + .component label("Foreground color:").gap(RightGap.SMALL) cell(ColorPanel(drawPanel.foregroundColor)).gap(RightGap.SMALL) lateinit var foregroundColorButton: JButton - foregroundColorButton = button("Change") { - ColorChooserService.instance.showDialog(project, foregroundColorButton, "Select Foreground Color", drawPanel.foregroundColor.get())?.let { - drawPanel.foregroundColor.set(it.toJBColor()) - generate() - } - }.component + foregroundColorButton = + button("Change") { + ColorChooserService.instance + .showDialog( + project, + foregroundColorButton, + "Select Foreground Color", + drawPanel.foregroundColor.get(), + ) + ?.let { + drawPanel.foregroundColor.set(it.toJBColor()) + generate() + } + } + .component } row { - val liveGenerationCheckBox = checkBox("Live generation") - .bindSelected(liveGeneration) - .whenStateChangedFromUi { - if (it) { - generate() + val liveGenerationCheckBox = + checkBox("Live generation") + .bindSelected(liveGeneration) + .whenStateChangedFromUi { + if (it) { + generate() + } } - } - .gap(RightGap.SMALL) + .gap(RightGap.SMALL) - button("â–¼ Generate") { generate(false) } - .enabledIf(liveGenerationCheckBox.selected.not()) + button("â–¼ Generate") { generate(false) }.enabledIf(liveGenerationCheckBox.selected.not()) - val exportToFileActions = ImageIO.getWriterFileSuffixes() - .map { fileFormat -> - DumbAwareAction.create("Export as $fileFormat") { exportToFile(fileFormat) } - } - .toTypedArray() + val exportToFileActions = + ImageIO.getWriterFileSuffixes() + .map { fileFormat -> + DumbAwareAction.create("Export as $fileFormat") { exportToFile(fileFormat) } + } + .toTypedArray() actionsButton( - actions = exportToFileActions, - actionPlace = BarcodeGenerator::class.java.name, - icon = AllIcons.Actions.MenuSaveall - ).label("Export:").visibleIf(contentErrorHolder.asComponentPredicate().not()) + actions = exportToFileActions, + actionPlace = BarcodeGenerator::class.java.name, + icon = AllIcons.Actions.MenuSaveall, + ) + .label("Export:") + .visibleIf(contentErrorHolder.asComponentPredicate().not()) } row { - cell(ScrollPaneFactory.createScrollPane(drawPanel, true)) - .label("Generated image:", LabelPosition.TOP) - .align(AlignY.TOP) - }.visibleIf(contentErrorHolder.asComponentPredicate().not()).resizableRow() + cell(ScrollPaneFactory.createScrollPane(drawPanel, true)) + .label("Generated image:", LabelPosition.TOP) + .align(AlignY.TOP) + } + .visibleIf(contentErrorHolder.asComponentPredicate().not()) + .resizableRow() } override fun afterBuildUi() { @@ -185,7 +209,7 @@ internal class BarcodeGenerator private constructor( generate(liveGeneration.get()) } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun generate(fromConfigChange: Boolean = true) { if (configuration.isResetting || (fromConfigChange && !liveGeneration.get())) { @@ -205,10 +229,14 @@ internal class BarcodeGenerator private constructor( val widthValue = formatConfiguration.width() val heightValue = formatConfiguration.height() - val newMatrix = multiFormatWriter.encode( - contentEditor.text, formatConfiguration.barcodeFormat, widthValue, heightValue, - formatConfiguration.createHints() - ) + val newMatrix = + multiFormatWriter.encode( + contentEditor.text, + formatConfiguration.barcodeFormat, + widthValue, + heightValue, + formatConfiguration.createHints(), + ) drawPanel.matrix.set(newMatrix) } catch (_: NumberFormatException) { contentErrorHolder.add("Input must be a number") @@ -220,7 +248,16 @@ internal class BarcodeGenerator private constructor( } private fun createContentEditor(parentDisposable: Disposable) = - DeveloperToolEditor("content", context, configuration, project, "Content", DeveloperToolEditor.EditorMode.INPUT, parentDisposable, contentText) + AdvancedEditor( + "content", + context, + configuration, + project, + "Content", + AdvancedEditor.EditorMode.INPUT, + parentDisposable, + contentText, + ) .onTextChangeFromUi { generate() } private fun exportToFile(fileFormat: String) { @@ -229,164 +266,154 @@ internal class BarcodeGenerator private constructor( val defaultFilename = "${format.get().name.lowercase()}_$timeStamp.$fileFormat" FileChooserFactory.getInstance() .createSaveFileDialog(fileSaverDescriptor, project) - .save(defaultFilename)?.file?.toPath()?.let { targetPath -> + .save(defaultFilename) + ?.file + ?.toPath() + ?.let { targetPath -> MatrixToImageWriter.writeToPath(drawPanel.matrix.get(), fileFormat, targetPath) } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class Format( val title: String, - val createConfiguration: (DeveloperToolConfiguration) -> FormatConfiguration + val createConfiguration: (DeveloperToolConfiguration) -> FormatConfiguration, ) { - AZTEC("Aztec 2D", { configuration -> - AztecCodeConfiguration(configuration) - }), - - QR_CODE("QR Code 2D", { configuration -> - QrCodeConfiguration(configuration) - }), - - DATA_MATRIX( - "Data Matrix 2D", { configuration -> - DataMatrixConfiguration(configuration) - } - ), - - PDF_417("PDF417", { configuration -> - Pdf417FormatConfiguration(configuration) - }), - + AZTEC("Aztec 2D", { configuration -> AztecCodeConfiguration(configuration) }), + QR_CODE("QR Code 2D", { configuration -> QrCodeConfiguration(configuration) }), + DATA_MATRIX("Data Matrix 2D", { configuration -> DataMatrixConfiguration(configuration) }), + PDF_417("PDF417", { configuration -> Pdf417FormatConfiguration(configuration) }), CODE_39( - "Code 39 1D", { configuration -> + "Code 39 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.CODE_39, configuration = configuration, supportsHeight = true, defaultHeight = 50, supportsMargin = true, - defaultMargin = 10 + defaultMargin = 10, ) - } + }, ), - CODE_93( - "Code 93 1D", { configuration -> + "Code 93 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.CODE_93, configuration = configuration, supportsHeight = true, defaultHeight = 50, supportsMargin = true, - defaultMargin = 10 + defaultMargin = 10, ) - } + }, ), - CODE_128( - "Code 128 1D", { configuration -> + "Code 128 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.CODE_128, configuration = configuration, supportsHeight = true, defaultHeight = 50, supportsMargin = true, - defaultMargin = 10 + defaultMargin = 10, ) - } + }, ), - EAN_8( - "EAN-8 1D", { configuration -> + "EAN-8 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.EAN_8, configuration = configuration, supportsHeight = true, defaultHeight = 50, supportsMargin = true, - defaultMargin = 10 + defaultMargin = 10, ) - } + }, ), - EAN_13( - "EAN-13 1D", { configuration -> + "EAN-13 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.EAN_13, configuration = configuration, supportsHeight = true, defaultHeight = 50, supportsMargin = true, - defaultMargin = 10 + defaultMargin = 10, ) - } + }, ), - ITF( - "ITF (Interleaved Two of Five) 1D", { configuration -> + "ITF (Interleaved Two of Five) 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.ITF, configuration = configuration, supportsHeight = true, defaultHeight = 50, supportsMargin = true, - defaultMargin = 10 + defaultMargin = 10, ) - } + }, ), - CODABAR( - "CODABAR 1D", { configuration -> + "CODABAR 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.CODABAR, configuration = configuration, supportsHeight = true, defaultHeight = 50, supportsMargin = true, - defaultMargin = 10 + defaultMargin = 10, ) - } + }, ), - UPC_A( - "UPC-A 1D", { configuration -> + "UPC-A 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.UPC_A, configuration = configuration, supportsHeight = true, - defaultHeight = 50 + defaultHeight = 50, ) - } + }, ), - UPC_E( - "UPC-E 1D", { configuration -> + "UPC-E 1D", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.UPC_E, configuration = configuration, supportsHeight = true, - defaultHeight = 50 + defaultHeight = 50, ) - } + }, ), - UPC_EAN_EXTENSION( - "UPC/EAN extension", { configuration -> + "UPC/EAN extension", + { configuration -> FormatConfiguration( barcodeFormat = BarcodeFormat.UPC_EAN_EXTENSION, configuration = configuration, supportsHeight = true, - defaultHeight = 50 + defaultHeight = 50, ) - } + }, ); override fun toString(): String = title } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // @Suppress("UnstableApiUsage") private open class FormatConfiguration( @@ -400,71 +427,75 @@ internal class BarcodeGenerator private constructor( defaultMargin: Int = 1, private val supportsErrorCorrection: ErrorCorrectionSupport = UNSUPPORTED, defaultErrorCorrection: ErrorCorrection = ErrorCorrection.H, - private val comment: String? = null + private val comment: String? = null, ) { private val width = configuration.register("${barcodeFormat.name}-width", defaultWidth) private val height = configuration.register("${barcodeFormat.name}-height", defaultHeight) private val margin = configuration.register("${barcodeFormat.name}-margin", defaultMargin) - private val errorCorrection = configuration.register("${barcodeFormat.name}-errorCorrection", defaultErrorCorrection) + private val errorCorrection = + configuration.register("${barcodeFormat.name}-errorCorrection", defaultErrorCorrection) fun width() = width.get() + fun height() = height.get() fun Panel.buildConfigurationUi(visible: ComponentPredicate, onConfigurationChange: () -> Unit) { row { - if (supportsWidth) { - textField() - .label("Width:") - .bindIntTextImproved(width) - .validateLongValue(LongRange(10, 1000)) - .columns(COLUMNS_TINY) - .whenTextChangedFromUi { onConfigurationChange() } - } + if (supportsWidth) { + textField() + .label("Width:") + .bindIntTextImproved(width) + .validateLongValue(LongRange(10, 1000)) + .columns(COLUMNS_TINY) + .whenTextChangedFromUi { onConfigurationChange() } + } - if (supportsHeight) { - textField() - .label("Height:") - .bindIntTextImproved(height) - .validateLongValue(LongRange(1, 1000)) - .columns(COLUMNS_TINY) - .whenTextChangedFromUi { onConfigurationChange() } - } + if (supportsHeight) { + textField() + .label("Height:") + .bindIntTextImproved(height) + .validateLongValue(LongRange(1, 1000)) + .columns(COLUMNS_TINY) + .whenTextChangedFromUi { onConfigurationChange() } + } - if (supportsMargin) { - textField() - .label("Margin:") - .bindIntTextImproved(margin) - .validateLongValue(LongRange(0, 100)) - .columns(COLUMNS_TINY) - .whenTextChangedFromUi { onConfigurationChange() } - } + if (supportsMargin) { + textField() + .label("Margin:") + .bindIntTextImproved(margin) + .validateLongValue(LongRange(0, 100)) + .columns(COLUMNS_TINY) + .whenTextChangedFromUi { onConfigurationChange() } + } - if (supportsErrorCorrection != UNSUPPORTED) { - comboBox(ErrorCorrection.entries) - .label("Error correction:") - .bindItem(errorCorrection) - .onChanged { onConfigurationChange() } - } + if (supportsErrorCorrection != UNSUPPORTED) { + comboBox(ErrorCorrection.entries) + .label("Error correction:") + .bindItem(errorCorrection) + .onChanged { onConfigurationChange() } + } - if (comment != null) { - rowComment(comment = comment, maxLineLength = MAX_LINE_LENGTH_WORD_WRAP) + if (comment != null) { + rowComment(comment = comment, maxLineLength = MAX_LINE_LENGTH_WORD_WRAP) + } } - }.visibleIf(visible) + .visibleIf(visible) buildAdditionalConfigurationUi(visible, onConfigurationChange) } - open fun Panel.buildAdditionalConfigurationUi(visible: ComponentPredicate, onConfigurationChange: () -> Unit) { - } + open fun Panel.buildAdditionalConfigurationUi( + visible: ComponentPredicate, + onConfigurationChange: () -> Unit, + ) {} open fun createHints(): Map { val hints = mutableMapOf() hints[EncodeHintType.CHARACTER_SET] = "utf-8" if (supportsErrorCorrection == LEVEL_ENUM_NAME) { hints[EncodeHintType.ERROR_CORRECTION] = errorCorrection.get().level.name - } - else if (supportsErrorCorrection == LEVEL_BITS) { + } else if (supportsErrorCorrection == LEVEL_BITS) { hints[EncodeHintType.ERROR_CORRECTION] = errorCorrection.get().level.bits } @@ -476,99 +507,115 @@ internal class BarcodeGenerator private constructor( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class ErrorCorrectionSupport { - UNSUPPORTED, LEVEL_ENUM_NAME, LEVEL_BITS + UNSUPPORTED, + LEVEL_ENUM_NAME, + LEVEL_BITS, } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class Pdf417FormatConfiguration( - configuration: DeveloperToolConfiguration - ) : FormatConfiguration( - barcodeFormat = BarcodeFormat.PDF_417, - configuration = configuration, - supportsWidth = true, - defaultWidth = 250, - supportsHeight = true, - defaultHeight = 50, - supportsMargin = true, - defaultMargin = 5, - supportsErrorCorrection = LEVEL_BITS, - comment = "The actual size will be calculated on a scale factor which is based on the relationship between the width and height value. Use a larger height than width value to rotate the barcode." - ) { + // -- Inner Type ---------------------------------------------------------- // - private val useCompactMode = configuration.register("${BarcodeFormat.PDF_417}-compactMode", false) - private val compactModeType = configuration.register("${BarcodeFormat.PDF_417}-compactionModeType", Compaction.AUTO) + private class Pdf417FormatConfiguration(configuration: DeveloperToolConfiguration) : + FormatConfiguration( + barcodeFormat = BarcodeFormat.PDF_417, + configuration = configuration, + supportsWidth = true, + defaultWidth = 250, + supportsHeight = true, + defaultHeight = 50, + supportsMargin = true, + defaultMargin = 5, + supportsErrorCorrection = LEVEL_BITS, + comment = + "The actual size will be calculated on a scale factor which is based on the relationship between the width and height value. Use a larger height than width value to rotate the barcode.", + ) { + + private val useCompactMode = + configuration.register("${BarcodeFormat.PDF_417}-compactMode", false) + private val compactModeType = + configuration.register("${BarcodeFormat.PDF_417}-compactionModeType", Compaction.AUTO) private val insertEcis = configuration.register("${BarcodeFormat.PDF_417}-insertEcis", false) - private val setDimensions = configuration.register("${BarcodeFormat.PDF_417}-setDimensions", false) + private val setDimensions = + configuration.register("${BarcodeFormat.PDF_417}-setDimensions", false) private val minColumns = configuration.register("${BarcodeFormat.PDF_417}-minColumns", 1) private val maxColumns = configuration.register("${BarcodeFormat.PDF_417}-maxColumns", 50) private val minRows = configuration.register("${BarcodeFormat.PDF_417}-minRows", 1) private val maxRows = configuration.register("${BarcodeFormat.PDF_417}-minRows", 50) @Suppress("UnstableApiUsage") - override fun Panel.buildAdditionalConfigurationUi(visible: ComponentPredicate, onConfigurationChange: () -> Unit) { + override fun Panel.buildAdditionalConfigurationUi( + visible: ComponentPredicate, + onConfigurationChange: () -> Unit, + ) { row { - val useCompactModeCheckBox = checkBox("PDF417 compact mode:") - .bindSelected(useCompactMode) - .onChanged { onConfigurationChange() } - .gap(RightGap.SMALL) - - val compactionRenderer = textListCellRenderer { - it?.name?.lowercase()?.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } ?: throw IllegalArgumentException() + val useCompactModeCheckBox = + checkBox("PDF417 compact mode:") + .bindSelected(useCompactMode) + .onChanged { onConfigurationChange() } + .gap(RightGap.SMALL) + + val compactionRenderer = + textListCellRenderer { + it?.name?.lowercase()?.replaceFirstChar { char -> char.titlecase(Locale.ROOT) } + ?: throw IllegalArgumentException() + } + comboBox(Compaction.entries, compactionRenderer) + .bindItem(compactModeType) + .whenItemSelectedFromUi { onConfigurationChange() } + .enabledIf(useCompactModeCheckBox.selected) + + checkBox("Automatically insert ECIs").bindSelected(insertEcis).whenStateChangedFromUi { + onConfigurationChange() + } } - comboBox(Compaction.entries, compactionRenderer) - .bindItem(compactModeType) - .whenItemSelectedFromUi { onConfigurationChange() } - .enabledIf(useCompactModeCheckBox.selected) - - checkBox("Automatically insert ECIs") - .bindSelected(insertEcis) - .whenStateChangedFromUi { onConfigurationChange() } - }.visibleIf(visible) + .visibleIf(visible) lateinit var limitDimensionsCheckbox: Cell row { - limitDimensionsCheckbox = checkBox("Limit dimensions:") - .bindSelected(setDimensions) - .whenStateChangedFromUi { onConfigurationChange() } - }.visibleIf(visible) + limitDimensionsCheckbox = + checkBox("Limit dimensions:").bindSelected(setDimensions).whenStateChangedFromUi { + onConfigurationChange() + } + } + .visibleIf(visible) row { - textField() - .label("Min. col.:") - .bindIntTextImproved(minColumns) - .validateLongValue(LongRange(1, 1000)) - .validateMinMaxValueRelation(ValidateMinIntValueSide.MIN) { maxColumns.get() } - .columns(COLUMNS_TINY) - .gap(RightGap.SMALL) - .whenTextChangedFromUi { onConfigurationChange() } - textField() - .label("Max. col.:") - .bindIntTextImproved(maxColumns) - .validateLongValue(LongRange(1, 1000)) - .validateMinMaxValueRelation(ValidateMinIntValueSide.MAX) { minColumns.get() } - .columns(COLUMNS_TINY) - .whenTextChangedFromUi { onConfigurationChange() } - - textField() - .label("Min. rows:") - .bindIntTextImproved(minRows) - .validateLongValue(LongRange(1, 1000)) - .validateMinMaxValueRelation(ValidateMinIntValueSide.MIN) { maxRows.get() } - .columns(COLUMNS_TINY) - .gap(RightGap.SMALL) - .whenTextChangedFromUi { onConfigurationChange() } - textField() - .label("Max. rows:") - .bindIntTextImproved(maxRows) - .validateLongValue(LongRange(1, 1000)) - .validateMinMaxValueRelation(ValidateMinIntValueSide.MAX) { minRows.get() } - .columns(COLUMNS_TINY) - .whenTextChangedFromUi { onConfigurationChange() } - }.visibleIf(visible).enabledIf(limitDimensionsCheckbox.selected) + textField() + .label("Min. col.:") + .bindIntTextImproved(minColumns) + .validateLongValue(LongRange(1, 1000)) + .validateMinMaxValueRelation(ValidateMinIntValueSide.MIN) { maxColumns.get() } + .columns(COLUMNS_TINY) + .gap(RightGap.SMALL) + .whenTextChangedFromUi { onConfigurationChange() } + textField() + .label("Max. col.:") + .bindIntTextImproved(maxColumns) + .validateLongValue(LongRange(1, 1000)) + .validateMinMaxValueRelation(ValidateMinIntValueSide.MAX) { minColumns.get() } + .columns(COLUMNS_TINY) + .whenTextChangedFromUi { onConfigurationChange() } + + textField() + .label("Min. rows:") + .bindIntTextImproved(minRows) + .validateLongValue(LongRange(1, 1000)) + .validateMinMaxValueRelation(ValidateMinIntValueSide.MIN) { maxRows.get() } + .columns(COLUMNS_TINY) + .gap(RightGap.SMALL) + .whenTextChangedFromUi { onConfigurationChange() } + textField() + .label("Max. rows:") + .bindIntTextImproved(maxRows) + .validateLongValue(LongRange(1, 1000)) + .validateMinMaxValueRelation(ValidateMinIntValueSide.MAX) { minRows.get() } + .columns(COLUMNS_TINY) + .whenTextChangedFromUi { onConfigurationChange() } + } + .visibleIf(visible) + .enabledIf(limitDimensionsCheckbox.selected) } override fun createHints(): Map { @@ -579,18 +626,17 @@ internal class BarcodeGenerator private constructor( hints[EncodeHintType.PDF417_AUTO_ECI] = insertEcis.get() if (setDimensions.get()) { - hints[EncodeHintType.PDF417_DIMENSIONS] = Dimensions(minColumns.get(), maxColumns.get(), minRows.get(), maxRows.get()) + hints[EncodeHintType.PDF417_DIMENSIONS] = + Dimensions(minColumns.get(), maxColumns.get(), minRows.get(), maxRows.get()) } return hints } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class AztecCodeConfiguration( - configuration: DeveloperToolConfiguration - ) : + private class AztecCodeConfiguration(configuration: DeveloperToolConfiguration) : FormatConfiguration( barcodeFormat = BarcodeFormat.AZTEC, configuration = configuration, @@ -598,48 +644,52 @@ internal class BarcodeGenerator private constructor( defaultWidth = 250, supportsHeight = true, defaultHeight = 250, - supportsErrorCorrection = LEVEL_BITS + supportsErrorCorrection = LEVEL_BITS, ) { private val layers = configuration.register("${BarcodeFormat.AZTEC}-layers", 0) @Suppress("UnstableApiUsage") - override fun Panel.buildAdditionalConfigurationUi(visible: ComponentPredicate, onConfigurationChange: () -> Unit) { + override fun Panel.buildAdditionalConfigurationUi( + visible: ComponentPredicate, + onConfigurationChange: () -> Unit, + ) { row { - val renderer = textListCellRenderer { - when { - it == null -> throw IllegalArgumentException() - it < 0 -> "$it (compact)" - it == 0 -> "Minimum" - else -> it.toString() - } + val renderer = + textListCellRenderer { + when { + it == null -> throw IllegalArgumentException() + it < 0 -> "$it (compact)" + it == 0 -> "Minimum" + else -> it.toString() + } + } + comboBox(IntRange(-4, 32).toList(), renderer) + .label("Layers:") + .bindItem(layers) + .whenItemSelectedFromUi { onConfigurationChange() } } - comboBox(IntRange(-4, 32).toList(), renderer) - .label("Layers:") - .bindItem(layers) - .whenItemSelectedFromUi { onConfigurationChange() } - }.visibleIf(visible) + .visibleIf(visible) } override fun createHints(): Map = super.createHints() + (EncodeHintType.AZTEC_LAYERS to layers.get()) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class QrCodeConfiguration( - configuration: DeveloperToolConfiguration - ) : FormatConfiguration( - barcodeFormat = BarcodeFormat.QR_CODE, - configuration = configuration, - supportsWidth = true, - defaultWidth = 200, - supportsHeight = true, - defaultHeight = 200, - supportsMargin = true, - defaultMargin = 0, - supportsErrorCorrection = LEVEL_ENUM_NAME - ) { + // -- Inner Type ---------------------------------------------------------- // + + private class QrCodeConfiguration(configuration: DeveloperToolConfiguration) : + FormatConfiguration( + barcodeFormat = BarcodeFormat.QR_CODE, + configuration = configuration, + supportsWidth = true, + defaultWidth = 200, + supportsHeight = true, + defaultHeight = 200, + supportsMargin = true, + defaultMargin = 0, + supportsErrorCorrection = LEVEL_ENUM_NAME, + ) { private val version = configuration.register("${BarcodeFormat.QR_CODE}-version", 0) private val compactMode = configuration.register("${BarcodeFormat.QR_CODE}-compactMode", false) @@ -647,30 +697,41 @@ internal class BarcodeGenerator private constructor( private val maskPattern = configuration.register("${BarcodeFormat.QR_CODE}-maskPattern", -1) @Suppress("UnstableApiUsage") - override fun Panel.buildAdditionalConfigurationUi(visible: ComponentPredicate, onConfigurationChange: () -> Unit) { + override fun Panel.buildAdditionalConfigurationUi( + visible: ComponentPredicate, + onConfigurationChange: () -> Unit, + ) { row { - val compactModeCheckBox = checkBox("Compact mode") - .bindSelected(compactMode) - .whenStateChangedFromUi { onConfigurationChange() } - .gap(RightGap.SMALL) - - checkBox("Use GS1") - .bindSelected(gs1) - .whenStateChangedFromUi { onConfigurationChange() } - .enabledIf(compactModeCheckBox.selected) - - val versionRenderer = textListCellRenderer { if (it == 0) "Minimum" else it?.toString() ?: throw IllegalArgumentException() } - comboBox(IntRange(0, 40).toList(), versionRenderer) - .label("Version:") - .bindItem(version) - .whenItemSelectedFromUi { onConfigurationChange() } - - val maskPatternRenderer = textListCellRenderer { if (it == -1) "Best" else it?.toString() ?: throw IllegalArgumentException() } - comboBox(IntRange(-1, QRCode.NUM_MASK_PATTERNS - 1).toList(), maskPatternRenderer) - .label("Mask pattern:") - .bindItem(maskPattern) - .whenItemSelectedFromUi { onConfigurationChange() } - }.visibleIf(visible) + val compactModeCheckBox = + checkBox("Compact mode") + .bindSelected(compactMode) + .whenStateChangedFromUi { onConfigurationChange() } + .gap(RightGap.SMALL) + + checkBox("Use GS1") + .bindSelected(gs1) + .whenStateChangedFromUi { onConfigurationChange() } + .enabledIf(compactModeCheckBox.selected) + + val versionRenderer = + textListCellRenderer { + if (it == 0) "Minimum" else it?.toString() ?: throw IllegalArgumentException() + } + comboBox(IntRange(0, 40).toList(), versionRenderer) + .label("Version:") + .bindItem(version) + .whenItemSelectedFromUi { onConfigurationChange() } + + val maskPatternRenderer = + textListCellRenderer { + if (it == -1) "Best" else it?.toString() ?: throw IllegalArgumentException() + } + comboBox(IntRange(-1, QRCode.NUM_MASK_PATTERNS - 1).toList(), maskPatternRenderer) + .label("Mask pattern:") + .bindItem(maskPattern) + .whenItemSelectedFromUi { onConfigurationChange() } + } + .visibleIf(visible) } override fun createHints(): Map { @@ -691,72 +752,76 @@ internal class BarcodeGenerator private constructor( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class DataMatrixConfiguration( - configuration: DeveloperToolConfiguration - ) : FormatConfiguration( - barcodeFormat = BarcodeFormat.DATA_MATRIX, - configuration = configuration, - supportsWidth = true, - defaultWidth = 250, - supportsHeight = true, - defaultHeight = 250 - ) { + // -- Inner Type ---------------------------------------------------------- // + + private class DataMatrixConfiguration(configuration: DeveloperToolConfiguration) : + FormatConfiguration( + barcodeFormat = BarcodeFormat.DATA_MATRIX, + configuration = configuration, + supportsWidth = true, + defaultWidth = 250, + supportsHeight = true, + defaultHeight = 250, + ) { - private val compactMode = configuration.register("${BarcodeFormat.DATA_MATRIX}-compactMode", false) + private val compactMode = + configuration.register("${BarcodeFormat.DATA_MATRIX}-compactMode", false) private val gs1 = configuration.register("${BarcodeFormat.DATA_MATRIX}-gs1", false) private val forceC40 = configuration.register("${BarcodeFormat.DATA_MATRIX}-forceC40", false) - private val symbolShape = configuration.register( - "${BarcodeFormat.DATA_MATRIX}-symbolShape", - SymbolShapeHint.FORCE_NONE - ) + private val symbolShape = + configuration.register("${BarcodeFormat.DATA_MATRIX}-symbolShape", SymbolShapeHint.FORCE_NONE) @Suppress("UnstableApiUsage") - override fun Panel.buildAdditionalConfigurationUi(visible: ComponentPredicate, onConfigurationChange: () -> Unit) { + override fun Panel.buildAdditionalConfigurationUi( + visible: ComponentPredicate, + onConfigurationChange: () -> Unit, + ) { row { - val compactModeCheckBox = checkBox("Compact mode") - .bindSelected(compactMode) - .whenStateChangedFromUi { onConfigurationChange() } - .gap(RightGap.SMALL) - - checkBox("Use GS1") - .bindSelected(gs1) - .whenStateChangedFromUi { onConfigurationChange() } - .enabledIf(compactModeCheckBox.selected) - .gap(RightGap.SMALL) - - checkBox("Force C40") - .bindSelected(forceC40) - .whenStateChangedFromUi { onConfigurationChange() } - .enabledIf(compactModeCheckBox.selected.not()) - - val symbolShapeRenderer = textListCellRenderer { - when (it) { - SymbolShapeHint.FORCE_NONE -> "Automatically" - SymbolShapeHint.FORCE_SQUARE -> "Square" - SymbolShapeHint.FORCE_RECTANGLE -> "Rectangle" - else -> throw IllegalArgumentException() - } + val compactModeCheckBox = + checkBox("Compact mode") + .bindSelected(compactMode) + .whenStateChangedFromUi { onConfigurationChange() } + .gap(RightGap.SMALL) + + checkBox("Use GS1") + .bindSelected(gs1) + .whenStateChangedFromUi { onConfigurationChange() } + .enabledIf(compactModeCheckBox.selected) + .gap(RightGap.SMALL) + + checkBox("Force C40") + .bindSelected(forceC40) + .whenStateChangedFromUi { onConfigurationChange() } + .enabledIf(compactModeCheckBox.selected.not()) + + val symbolShapeRenderer = + textListCellRenderer { + when (it) { + SymbolShapeHint.FORCE_NONE -> "Automatically" + SymbolShapeHint.FORCE_SQUARE -> "Square" + SymbolShapeHint.FORCE_RECTANGLE -> "Rectangle" + else -> throw IllegalArgumentException() + } + } + comboBox(SymbolShapeHint.entries, symbolShapeRenderer) + .bindItem(symbolShape) + .label("Symbol shape:") + .whenItemSelectedFromUi { onConfigurationChange() } } - comboBox(SymbolShapeHint.entries, symbolShapeRenderer) - .bindItem(symbolShape) - .label("Symbol shape:") - .whenItemSelectedFromUi { onConfigurationChange() } - }.visibleIf(visible) + .visibleIf(visible) } override fun createHints(): Map = super.createHints() + - mapOf( - EncodeHintType.DATA_MATRIX_COMPACT to compactMode, - EncodeHintType.DATA_MATRIX_SHAPE to symbolShape.get(), - EncodeHintType.GS1_FORMAT to gs1.get(), - EncodeHintType.FORCE_C40 to forceC40.get() - ) + mapOf( + EncodeHintType.DATA_MATRIX_COMPACT to compactMode, + EncodeHintType.DATA_MATRIX_SHAPE to symbolShape.get(), + EncodeHintType.GS1_FORMAT to gs1.get(), + EncodeHintType.FORCE_C40 to forceC40.get(), + ) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class ErrorCorrection(val level: ErrorCorrectionLevel, val title: String) { @@ -768,7 +833,7 @@ internal class BarcodeGenerator private constructor( override fun toString(): String = title } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class DrawPanel(configuration: DeveloperToolConfiguration) : JPanel() { @@ -789,8 +854,14 @@ internal class BarcodeGenerator private constructor( repaint() parent?.repaint() } - backgroundColor.afterChange { revalidate(); repaint() } - foregroundColor.afterChange { revalidate(); repaint() } + backgroundColor.afterChange { + revalidate() + repaint() + } + foregroundColor.afterChange { + revalidate() + repaint() + } } fun revalidate2(parent: Container) { @@ -817,7 +888,7 @@ internal class BarcodeGenerator private constructor( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class ColorPanel(private val color: ObservableMutableProperty) : JPanel() { @@ -846,32 +917,33 @@ internal class BarcodeGenerator private constructor( private fun updateToolTipText() { val colorValue = color.get() - toolTipText = "#${toHexString(colorValue.red)}${toHexString(colorValue.green)}${toHexString(colorValue.blue)}" + toolTipText = + "#${toHexString(colorValue.red)}${toHexString(colorValue.green)}${toHexString(colorValue.blue)}" } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "QR Code/Barcode", - contentTitle = "QR Code/Barcode Generator" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = "QR Code/Barcode", + contentTitle = "QR Code/Barcode Generator", + ) override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> BarcodeGenerator) = { configuration -> - val formats: Map = Format.entries.associateWith { - it.createConfiguration(configuration) - } + val formats: Map = + Format.entries.associateWith { it.createConfiguration(configuration) } BarcodeGenerator(formats, context, configuration, project, parentDisposable) } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @@ -885,4 +957,4 @@ internal class BarcodeGenerator private constructor( private val timestampFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-SS") } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/LoremIpsumGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/LoremIpsumGenerator.kt new file mode 100644 index 00000000..90e460e5 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/LoremIpsumGenerator.kt @@ -0,0 +1,296 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator + +import ai.grazie.utils.capitalize +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.dsl.builder.COLUMNS_TINY +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.layout.ComboBoxPredicate +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ValidateMinIntValueSide.MAX +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ValidateMinIntValueSide.MIN +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindIntTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateMinMaxValueRelation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.LoremIpsumGenerator.TextMode.BULLETS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.LoremIpsumGenerator.TextMode.PARAGRAPHS +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.LoremIpsumGenerator.TextMode.WORDS +import java.security.SecureRandom +import kotlin.math.max +import kotlin.math.min + +class LoremIpsumGenerator( + project: Project?, + context: DeveloperUiToolContext, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, +) : + MultiLineTextGenerator( + generatedTextTitle = "Generated lorem ipsum", + configuration = configuration, + parentDisposable = parentDisposable, + context = context, + project = project, + ) { + // -- Properties ---------------------------------------------------------- // + + private var textMode = configuration.register("generatedTextKind", PARAGRAPHS) + private var numberOfValues = configuration.register("numberOfValues", 9) + private var minWordsInParagraph = + configuration.register("minWordsInParagraph", DEFAULT_MIN_PARAGRAPH_WORDS) + private var maxWordsInParagraph = + configuration.register("maxWordsInParagraph", DEFAULT_MAX_PARAGRAPH_WORDS) + private var minWordsInBullet = + configuration.register("minWordsInBullet", DEFAULT_MIN_BULLET_WORDS) + private var maxWordsInBullet = + configuration.register("maxWordsInBullet", DEFAULT_MAX_BULLET_WORDS) + private var startWithLoremIpsum = configuration.register("startWithLoremIpsum", true) + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun generate(): String = + when (textMode.get()) { + WORDS -> generateWords() + PARAGRAPHS -> generateParagraphs() + BULLETS -> generateBullets() + } + + override fun Panel.buildConfigurationUi() { + lateinit var textModeComboBox: ComboBox + row { + textField() + .bindIntTextImproved(numberOfValues) + .validateLongValue(LongRange(1, 999)) + .columns(COLUMNS_TINY) + .gap(RightGap.SMALL) + textModeComboBox = comboBox(TextMode.entries).bindItem(textMode).component + } + + row { + textField() + .label("Minimum words in paragraph:") + .bindIntTextImproved(minWordsInParagraph) + .validateLongValue(LongRange(1, 999)) + .columns(COLUMNS_TINY) + .validateMinMaxValueRelation(MIN) { maxWordsInParagraph.get() } + .gap(RightGap.SMALL) + textField() + .label("Maximum:") + .bindIntTextImproved(maxWordsInParagraph) + .validateLongValue(LongRange(1, 999)) + .columns(COLUMNS_TINY) + .validateMinMaxValueRelation(MAX) { minWordsInParagraph.get() } + } + .visibleIf(ComboBoxPredicate(textModeComboBox) { it == PARAGRAPHS }) + + row { + textField() + .label("Minimum words in bullet:") + .bindIntTextImproved(minWordsInBullet) + .validateLongValue(LongRange(1, 999)) + .columns(COLUMNS_TINY) + .validateMinMaxValueRelation(MIN) { maxWordsInBullet.get() } + .gap(RightGap.SMALL) + textField() + .label("Maximum:") + .bindIntTextImproved(maxWordsInBullet) + .validateLongValue(LongRange(1, 999)) + .columns(COLUMNS_TINY) + .validateMinMaxValueRelation(MAX) { minWordsInBullet.get() } + } + .visibleIf(ComboBoxPredicate(textModeComboBox) { it == BULLETS }) + + row { + checkBox("Start with iconic Lorem ipsum dolor sit amet…") + .bindSelected(startWithLoremIpsum) + } + } + + fun generateIconicText(atMostWords: Int, isSentence: Boolean): List { + val words = + ICONIC_LOREM_IPSUM_SENTENCE.subList(0, min(ICONIC_LOREM_IPSUM_SENTENCE.size, atMostWords)) + .toMutableList() + + if (isSentence) { + val wordsSize = words.size + // Comma + if (wordsSize > ICONIC_LOREM_IPSUM_SENTENCE_COMMA_INDEX + 1) { + words[ICONIC_LOREM_IPSUM_SENTENCE_COMMA_INDEX] = + "${words[ICONIC_LOREM_IPSUM_SENTENCE_COMMA_INDEX]}," + } + if (wordsSize >= 1) { + // Capitalize first character + words[0] = words[0].capitalize() + // Full stop + words[wordsSize - 1] = "${words[wordsSize - 1]}." + } + } + + return words + } + + // -- Private Methods ----------------------------------------------------- // + + private fun generateParagraphs() = + IntRange(0, numberOfValues.get() - 1).joinToString(PARAGRAPH_SEPARATOR) { paragraphIndex -> + val totalWordsInParagraph = + SECURE_RANDOM.nextInt(minWordsInParagraph.get(), maxWordsInParagraph.get() + 1) + + val initialWords = + if (paragraphIndex == 0 && startWithLoremIpsum.get()) { + generateIconicText(totalWordsInParagraph, true) + } else { + emptyList() + } + + createSentences(initialWords, totalWordsInParagraph).joinToString(WORDS_SEPARATOR) + } + + private fun generateWords(): String { + val words = mutableListOf() + + if (startWithLoremIpsum.get()) { + words.addAll(generateIconicText(numberOfValues.get(), false)) + } + + if (words.size < numberOfValues.get()) { + words.addAll(getRandomWords(numberOfValues.get() - words.size)) + } + + return words.joinToString(WORDS_SEPARATOR) + } + + private fun generateBullets() = + IntRange(0, numberOfValues.get() - 1).joinToString(BULLET_SEPARATOR) { bulletIndex -> + val words = mutableListOf() + + val totalWordsInBullet = SECURE_RANDOM.nextInt(minWordsInBullet.get(), maxWordsInBullet.get()) + + if (bulletIndex == 0 && startWithLoremIpsum.get()) { + words.addAll(generateIconicText(totalWordsInBullet, true)) + } + + // Avoid a single word sentence. + words.addAll(createSentences(words, totalWordsInBullet)) + + words[0] = "$BULLET_SYMBOL ${words[0]}" + + words.joinToString(WORDS_SEPARATOR) + } + + private fun getRandomWords(words: Int): List = + IntRange(0, words - 1).asSequence().map { getRandomWord() }.toList() + + private fun getRandomWord() = LOREM_IPSUM_WORDS[SECURE_RANDOM.nextInt(LOREM_IPSUM_WORDS.size)] + + private fun createSentence(words: List): List { + // Divide sentence in n-1 fragments (the last fragment does not get a comma). + val fragments = Math.floorDiv(words.size, TEXT_FRAGMENT_LENGTH) - 1 + val indiciesOfWordsWithCommas = + IntRange(0, fragments - 1) + // Randomly decide with a 2/3 change to put comma in fragment + .filter { SECURE_RANDOM.nextInt(1, 4) != 3 } + // Randomly decide index after the first word + .map { fragmentIndex -> + val commaIndexInFragment = SECURE_RANDOM.nextInt(1, TEXT_FRAGMENT_LENGTH) + (TEXT_FRAGMENT_LENGTH * fragmentIndex) + commaIndexInFragment + } + .toSet() + + return words.mapIndexed { i: Int, rawWord: String -> + var word = rawWord + if (i == 0) { + word = word.capitalize() + } + if (i == words.size - 1) { + word = "$word." + } + if (indiciesOfWordsWithCommas.contains(i)) { + word = "$word," + } + word + } + } + + private fun createSentences(initialWords: List, totalWords: Int): List { + val words = initialWords.toMutableList() + + while (words.size < totalWords) { + val remainingWords = totalWords - words.size + // With the max we ensue that there are at least `MIN_SENTENCE_WORDS` words. + val minSentenceWords = max(min(MIN_SENTENCE_WORDS, remainingWords), MIN_SENTENCE_WORDS) + val maxSentenceWords = max(min(MAX_SENTENCE_WORDS, remainingWords), MIN_SENTENCE_WORDS) + val sentenceWords = + if (minSentenceWords == maxSentenceWords) minSentenceWords + else SECURE_RANDOM.nextInt(minSentenceWords, maxSentenceWords) + val elements = createSentence(getRandomWords(sentenceWords)) + words.addAll(elements) + } + + return words + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class TextMode(val title: String) { + + PARAGRAPHS("Paragraphs"), + WORDS("Words"), + BULLETS("Bullets"); + + override fun toString(): String = title + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Lorem Ipsum", contentTitle = "Lorem Ipsum Generator") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> LoremIpsumGenerator) = { configuration -> + LoremIpsumGenerator(project, context, configuration, parentDisposable) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val SECURE_RANDOM = SecureRandom() + private val ICONIC_LOREM_IPSUM_SENTENCE = + listOf("lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit") + private const val ICONIC_LOREM_IPSUM_SENTENCE_COMMA_INDEX = 4 + private val LOREM_IPSUM_WORDS: List by lazy { + LoremIpsumGenerator::class + .java + .getResource("/dev/turingcomplete/intellijdevelopertoolsplugin/lorem-ipsum.txt")!! + .readText() + .lines() + } + private const val DEFAULT_MIN_PARAGRAPH_WORDS = 20 + private const val DEFAULT_MAX_PARAGRAPH_WORDS = 100 + private const val DEFAULT_MIN_BULLET_WORDS = 10 + private const val DEFAULT_MAX_BULLET_WORDS = 30 + private const val MIN_SENTENCE_WORDS = 3 + private const val MAX_SENTENCE_WORDS = 40 + private const val TEXT_FRAGMENT_LENGTH = 8 + private val PARAGRAPH_SEPARATOR = System.lineSeparator().repeat(2) + private const val WORDS_SEPARATOR = " " + private const val BULLET_SYMBOL = "-" + private val BULLET_SEPARATOR = System.lineSeparator().repeat(2) + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/MultiLineTextGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/MultiLineTextGenerator.kt similarity index 51% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/MultiLineTextGenerator.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/MultiLineTextGenerator.kt index 6c84a220..3d6cbc46 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/MultiLineTextGenerator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/MultiLineTextGenerator.kt @@ -1,39 +1,35 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.Panel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor abstract class MultiLineTextGenerator( private val generatedTextTitle: String, private val context: DeveloperUiToolContext, private val project: Project?, private val configuration: DeveloperToolConfiguration, - parentDisposable: Disposable + parentDisposable: Disposable, ) : DeveloperUiTool(parentDisposable), DeveloperToolConfiguration.ChangeListener { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // - private val generatedTextEditor: DeveloperToolEditor by lazy { createGeneratedTextEditor() } + private val generatedTextEditor: AdvancedEditor by lazy { createGeneratedTextEditor() } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // final override fun Panel.buildUi() { buildConfigurationUi() - row { - button("Regenerate") { doGenerate() } - } + row { button("Regenerate") { doGenerate() } } - row { - cell(generatedTextEditor.component).align(Align.FILL) - }.resizableRow() + row { cell(generatedTextEditor.component).align(Align.FILL) }.resizableRow() } override fun afterBuildUi() { @@ -64,27 +60,25 @@ abstract class MultiLineTextGenerator( configuration.removeChangeListener(this) } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun doGenerate() { if (validate().isEmpty()) { - generatedTextEditor.apply { - text = generate() - } + generatedTextEditor.apply { text = generate() } } } - private fun createGeneratedTextEditor() = DeveloperToolEditor( - id = "generated-text", - title = generatedTextTitle, - editorMode = DeveloperToolEditor.EditorMode.OUTPUT, - parentDisposable = parentDisposable, - project = project, - context = context, - configuration = configuration - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // + private fun createGeneratedTextEditor() = + AdvancedEditor( + id = "generated-text", + title = generatedTextTitle, + editorMode = AdvancedEditor.EditorMode.OUTPUT, + parentDisposable = parentDisposable, + project = project, + context = context, + configuration = configuration, + ) + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // } - diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/NanoIdGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/NanoIdGenerator.kt new file mode 100644 index 00000000..2d0d41c0 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/NanoIdGenerator.kt @@ -0,0 +1,47 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator + +import com.aventrix.jnanoid.jnanoid.NanoIdUtils +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation + +class NanoIdGenerator( + project: Project?, + context: DeveloperUiToolContext, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, +) : + OneLineTextGenerator( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + ) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun generate(): String = NanoIdUtils.randomNanoId() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Nano ID", contentTitle = "Nano ID Generator") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> NanoIdGenerator) = { configuration -> + NanoIdGenerator(project, context, configuration, parentDisposable) + } + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/OneLineTextGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/OneLineTextGenerator.kt similarity index 53% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/OneLineTextGenerator.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/OneLineTextGenerator.kt index ebfdd7bc..48485eea 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/OneLineTextGenerator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/OneLineTextGenerator.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator import com.intellij.icons.AllIcons import com.intellij.openapi.Disposable @@ -17,26 +17,26 @@ import com.intellij.ui.dsl.builder.actionButton import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.columns import com.intellij.util.Alarm -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.BooleanComponentPredicate -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.CopyAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.CopyAction.Companion.CONTENT_DATA_KEY -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor.EditorMode.OUTPUT -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.monospaceFont -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.not -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.not +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode.OUTPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.BooleanComponentPredicate +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.CopyAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.CopyAction.Companion.CONTENT_DATA_KEY +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.monospaceFont import java.awt.Font abstract class OneLineTextGenerator( private val project: Project?, private val context: DeveloperUiToolContext, private val configuration: DeveloperToolConfiguration, - parentDisposable: Disposable + parentDisposable: Disposable, ) : DeveloperUiTool(parentDisposable), DeveloperToolConfiguration.ChangeListener { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // protected val supportsBulkGeneration = BooleanComponentPredicate(true) @@ -44,8 +44,8 @@ abstract class OneLineTextGenerator( private val invalidConfiguration = AtomicBooleanProperty(false) private val generationAlarm by lazy { Alarm(parentDisposable) } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // final override fun Panel.buildUi() { buildConfigurationUi() @@ -87,20 +87,20 @@ abstract class OneLineTextGenerator( configuration.removeChangeListener(this) } - override fun getData(dataId: String): Any? = when { - CONTENT_DATA_KEY.`is`(dataId) -> generatedText.get() - else -> super.getData(dataId) - } + override fun getData(dataId: String): Any? = + when { + CONTENT_DATA_KEY.`is`(dataId) -> generatedText.get() + else -> super.getData(dataId) + } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun doGenerate() { val generate: () -> Unit = { if (validate().isEmpty()) { generatedText.set(generate()) invalidConfiguration.set(false) - } - else { + } else { invalidConfiguration.set(true) } } @@ -112,50 +112,64 @@ abstract class OneLineTextGenerator( private fun Panel.buildBulkGenerationUi() { collapsibleGroup("Bulk Generation", false) { - val resultEditor = DeveloperToolEditor( - id = "bulk-generation", title = null, editorMode = OUTPUT, - configuration = configuration, project = project, context = context, - parentDisposable = parentDisposable - ) - - row { - label("Number of values:").gap(RightGap.SMALL) - - val numberOfValuesTextField = intTextField(IntRange(1, 99999)).columns(COLUMNS_TINY).applyToComponent { - text = DEFAULT_NUMBER_OF_VALUES - }.gap(RightGap.SMALL) - - button("Generate") { - configuration - if (validate().isEmpty()) { - resultEditor.text = IntRange(1, numberOfValuesTextField.component.text.toInt()) - .joinToString(System.lineSeparator()) { generate() } + val resultEditor = + AdvancedEditor( + id = "bulk-generation", + title = null, + editorMode = OUTPUT, + configuration = configuration, + project = project, + context = context, + parentDisposable = parentDisposable, + ) + + row { + label("Number of values:").gap(RightGap.SMALL) + + val numberOfValuesTextField = + intTextField(IntRange(1, 99999)) + .columns(COLUMNS_TINY) + .applyToComponent { text = DEFAULT_NUMBER_OF_VALUES } + .gap(RightGap.SMALL) + + button("Generate") { + configuration + if (validate().isEmpty()) { + resultEditor.text = + IntRange(1, numberOfValuesTextField.component.text.toInt()).joinToString( + System.lineSeparator() + ) { + generate() + } + } } } - } - row { - cell(resultEditor.component).align(Align.FILL) - }.resizableRow() - }.resizableRow().bottomGap(BottomGap.MEDIUM).visibleIf(supportsBulkGeneration) + row { cell(resultEditor.component).align(Align.FILL) }.resizableRow() + } + .resizableRow() + .bottomGap(BottomGap.MEDIUM) + .visibleIf(supportsBulkGeneration) } private fun Panel.buildGeneratedValueUi() { row { - label("") - .bindText(generatedText) - .monospaceFont(scale = 1.7f, style = Font.BOLD) - .gap(RightGap.SMALL) - actionButton(CopyAction()).gap(RightGap.SMALL) - actionButton(RegenerateAction { doGenerate() }) - }.visibleIf(invalidConfiguration.not()) + label("") + .bindText(generatedText) + .monospaceFont(scale = 1.7f, style = Font.BOLD) + .gap(RightGap.SMALL) + actionButton(CopyAction()).gap(RightGap.SMALL) + actionButton(RegenerateAction { doGenerate() }) + } + .visibleIf(invalidConfiguration.not()) row { - icon(AllIcons.General.BalloonError).gap(RightGap.SMALL) - label("Invalid configuration").bold() - }.visibleIf(invalidConfiguration) + icon(AllIcons.General.BalloonError).gap(RightGap.SMALL) + label("Invalid configuration").bold() + } + .visibleIf(invalidConfiguration) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class RegenerateAction(private val generateContent: () -> Unit) : DumbAwareAction("Regenerate", null, AllIcons.Actions.Refresh) { @@ -167,13 +181,13 @@ abstract class OneLineTextGenerator( override fun getActionUpdateThread() = ActionUpdateThread.BGT } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { private const val DEFAULT_NUMBER_OF_VALUES = "10" - //val codeFont: Font = JBFont.MONOSPACED EditorColorsManager.getInstance().globalScheme.editorFontName + // val codeFont: Font = JBFont.MONOSPACED + // EditorColorsManager.getInstance().globalScheme.editorFontName } } - diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/PasswordGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/PasswordGenerator.kt similarity index 50% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/PasswordGenerator.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/PasswordGenerator.kt index 0e99ff2d..d2d85267 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/PasswordGenerator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/PasswordGenerator.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project @@ -11,33 +11,33 @@ import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.layout.ValidationInfoBuilder import com.intellij.ui.layout.selected -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bindIntTextImproved -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateLongValue -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.PasswordGenerator.LettersMode.ASCII_ALPHABET -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.PasswordGenerator.LettersMode.ASCII_ALPHABET_ONLY_LOWERCASE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.PasswordGenerator.LettersMode.ASCII_ALPHABET_ONLY_UPPERCASE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.PasswordGenerator.LettersMode.NONE -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import io.ktor.util.* -import org.apache.commons.text.RandomStringGenerator +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindIntTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.PasswordGenerator.LettersMode.ASCII_ALPHABET +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.PasswordGenerator.LettersMode.ASCII_ALPHABET_ONLY_LOWERCASE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.PasswordGenerator.LettersMode.ASCII_ALPHABET_ONLY_UPPERCASE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.PasswordGenerator.LettersMode.NONE import java.security.SecureRandom import javax.swing.JComponent +import org.apache.commons.text.RandomStringGenerator -internal class PasswordGenerator( +class PasswordGenerator( project: Project?, context: DeveloperUiToolContext, configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : OneLineTextGenerator( - context = context, - project = project, - configuration = configuration, - parentDisposable = parentDisposable -) { - // -- Properties -------------------------------------------------------------------------------------------------- // + parentDisposable: Disposable, +) : + OneLineTextGenerator( + context = context, + project = project, + configuration = configuration, + parentDisposable = parentDisposable, + ) { + // -- Properties ---------------------------------------------------------- // private var length = configuration.register("length", DEFAULT_LENGTH) private var lettersMode = configuration.register("lettersMode", DEFAULT_LETTERS_MODE) @@ -45,44 +45,49 @@ internal class PasswordGenerator( private var addSymbols = configuration.register("addSymbols", DEFAULT_ADD_SYMBOLS) private var symbols = configuration.register("symbols", DEFAULT_SYMBOLS) - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // @Suppress("UnstableApiUsage") override fun Panel.buildConfigurationUi() { rowsRange { row { - textField() - .label("Length:") - .validateLongValue(LongRange(1, 100)) - .bindIntTextImproved(length) - }.layout(RowLayout.PARENT_GRID) + textField() + .label("Length:") + .validateLongValue(LongRange(1, 100)) + .bindIntTextImproved(length) + } + .layout(RowLayout.PARENT_GRID) row { - comboBox(LettersMode.entries) - .label("Letters:") - .bindItem(lettersMode) - .validationInfo(validateAtLeastOneCharacter()) - .component - }.layout(RowLayout.PARENT_GRID) + comboBox(LettersMode.entries) + .label("Letters:") + .bindItem(lettersMode) + .validationInfo(validateAtLeastOneCharacter()) + .component + } + .layout(RowLayout.PARENT_GRID) row { - checkBox("Add digits") - .bindSelected(addDigits) - .validationInfo(validateAtLeastOneCharacter()) - .align(Align.FILL) - }.layout(RowLayout.PARENT_GRID) + checkBox("Add digits") + .bindSelected(addDigits) + .validationInfo(validateAtLeastOneCharacter()) + .align(Align.FILL) + } + .layout(RowLayout.PARENT_GRID) row { - val addSymbolsCheckBox = checkBox("Add symbols:") - .bindSelected(addSymbols) - .validationInfo(validateAtLeastOneCharacter()) - .component - expandableTextField() - .bindText(symbols) - .enabledIf(addSymbolsCheckBox.selected) - .align(Align.FILL) - }.layout(RowLayout.PARENT_GRID) + val addSymbolsCheckBox = + checkBox("Add symbols:") + .bindSelected(addSymbols) + .validationInfo(validateAtLeastOneCharacter()) + .component + expandableTextField() + .bindText(symbols) + .enabledIf(addSymbolsCheckBox.selected) + .align(Align.FILL) + } + .layout(RowLayout.PARENT_GRID) } } @@ -92,26 +97,27 @@ internal class PasswordGenerator( return RandomStringGenerator.Builder() .usingRandom { SECURE_RANDOM.nextInt(it) } .selectFrom(*characters) - .build() + .get() .generate(length, length) } - private fun validateAtLeastOneCharacter(): ValidationInfoBuilder.(JComponent) -> ValidationInfo? = { - if (collectCharacters().isEmpty()) { - ValidationInfo("At least one character must be provided") - } - else { - null + private fun validateAtLeastOneCharacter(): ValidationInfoBuilder.(JComponent) -> ValidationInfo? = + { + if (collectCharacters().isEmpty()) { + ValidationInfo("At least one character must be provided") + } else { + null + } } - } private fun collectCharacters(): CharArray { - var characters = when (lettersMode.get()) { - NONE -> "" - ASCII_ALPHABET -> ASCII_ALPHABET_LOWERCASE + ASCII_ALPHABET_UPPERCASE - ASCII_ALPHABET_ONLY_LOWERCASE -> ASCII_ALPHABET_LOWERCASE - ASCII_ALPHABET_ONLY_UPPERCASE -> ASCII_ALPHABET_UPPERCASE - } + var characters = + when (lettersMode.get()) { + NONE -> "" + ASCII_ALPHABET -> ASCII_ALPHABET_LOWERCASE + ASCII_ALPHABET_UPPERCASE + ASCII_ALPHABET_ONLY_LOWERCASE -> ASCII_ALPHABET_LOWERCASE + ASCII_ALPHABET_ONLY_UPPERCASE -> ASCII_ALPHABET_UPPERCASE + } if (addDigits.get()) { characters += DIGITS @@ -124,8 +130,8 @@ internal class PasswordGenerator( return characters.toCharArray() } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class LettersMode(val title: String) { @@ -137,25 +143,26 @@ internal class PasswordGenerator( override fun toString(): String = title } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Password Generator", - contentTitle = "Password Generator" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = "Password Generator", + contentTitle = "Password Generator", + ) override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> PasswordGenerator) = { configuration -> PasswordGenerator(project, context, configuration, parentDisposable) } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @@ -171,4 +178,4 @@ internal class PasswordGenerator( private const val ASCII_ALPHABET_UPPERCASE = "ACBDEFGHIJKLMNOPQRSTUVWXYZ" private const val DIGITS = "0123456789" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/UlidGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/UlidGenerator.kt similarity index 50% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/UlidGenerator.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/UlidGenerator.kt index 4318bc70..ce6f1e95 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/UlidGenerator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/UlidGenerator.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator import com.github.f4b6a3.ulid.Ulid import com.github.f4b6a3.ulid.UlidCreator @@ -18,29 +18,31 @@ import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.columns -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.CopyAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bindLongTextImproved -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.not -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateLongValue -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.not +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.CopyAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindLongTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue import java.time.format.DateTimeFormatter class UlidGenerator( project: Project?, context: DeveloperUiToolContext, configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : OneLineTextGenerator( - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -), DataProvider { - // -- Properties -------------------------------------------------------------------------------------------------- // + parentDisposable: Disposable, +) : + OneLineTextGenerator( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + ), + DataProvider { + // -- Properties ---------------------------------------------------------- // private val generateMonotonicUlid = configuration.register("generateMonotonicUlid", false) private val ulidFormat = configuration.register("ulidFormat", UlidFormat.NONE) @@ -52,39 +54,31 @@ class UlidGenerator( private val parsedUlIdTimestamp = ValueProperty("") private val parsedUlIdTransformed = ValueProperty("") - // -- Initialization ---------------------------------------------------------------------------------------------- // - + // -- Initialization ------------------------------------------------------ // + init { parseUlidInput.afterChange { parseUlid() } parseUlidTransformFormat.afterChange { parseUlid() } parseUlid() } - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Exported Methods ---------------------------------------------------- // override fun Panel.buildConfigurationUi() { + row { comboBox(UlidFormat.entries).label("Format:").bindItem(ulidFormat) } row { - comboBox(UlidFormat.entries) - .label("Format:") - .bindItem(ulidFormat) - } - row { - checkBox("Monotonic") - .bindSelected(generateMonotonicUlid) - .gap(RightGap.SMALL) - contextHelp("If selected, the random component is incremented for each new ULID generated in the same millisecond. Otherwise, the random component is reset for each new ULID generated.") + checkBox("Monotonic").bindSelected(generateMonotonicUlid).gap(RightGap.SMALL) + contextHelp( + "If selected, the random component is incremented for each new ULID generated in the same millisecond. Otherwise, the random component is reset for each new ULID generated." + ) } buttonsGroup { + row { radioButton("Use current Unix timestamp").bindSelected(useIndividualTime.not()) } row { - radioButton("Use current Unix timestamp") - .bindSelected(useIndividualTime.not()) - } - row { - radioButton("Use individual timestamp:") - .bindSelected(useIndividualTime) - .gap(RightGap.SMALL) - textField().validateLongValue(LongRange(0, Long.MAX_VALUE)) + radioButton("Use individual timestamp:").bindSelected(useIndividualTime).gap(RightGap.SMALL) + textField() + .validateLongValue(LongRange(0, Long.MAX_VALUE)) .gap(RightGap.SMALL) .enabledIf(useIndividualTime) .bindLongTextImproved(individualTime) @@ -98,57 +92,57 @@ class UlidGenerator( row { expandableTextField() .bindText(parseUlidInput) - .validationOnInput { if (!Ulid.isValid(it.text)) ValidationInfo("Invalid ULID") else null } + .validationOnInput { + if (!Ulid.isValid(it.text)) ValidationInfo("Invalid ULID") else null + } .align(Align.FILL) } row { - label("") - .label("Timestamp:") - .bindText(parsedUlIdTimestamp) - .gap(RightGap.SMALL) + label("").label("Timestamp:").bindText(parsedUlIdTimestamp).gap(RightGap.SMALL) actionButton(CopyAction(parsedUlIdTimestampDataKey), UlidGenerator::class.java.name) } row { - comboBox(UlidFormat.entries.filter { it != UlidFormat.NONE }) - .label("Transform to:") - .bindItem(parseUlidTransformFormat) - }.bottomGap(BottomGap.NONE) + comboBox(UlidFormat.entries.filter { it != UlidFormat.NONE }) + .label("Transform to:") + .bindItem(parseUlidTransformFormat) + } + .bottomGap(BottomGap.NONE) row { - label("") - .bindText(parsedUlIdTransformed) - .gap(RightGap.SMALL) - actionButton(CopyAction(parsedUlIdTransformedDataKey), UlidGenerator::class.java.name) - }.topGap(TopGap.NONE) + label("").bindText(parsedUlIdTransformed).gap(RightGap.SMALL) + actionButton(CopyAction(parsedUlIdTransformedDataKey), UlidGenerator::class.java.name) + } + .topGap(TopGap.NONE) } } override fun generate(): String { - val ulid: Ulid = if (generateMonotonicUlid.get()) { - if (useIndividualTime.get()) { - UlidCreator.getMonotonicUlid(individualTime.get()) - } - else { - UlidCreator.getMonotonicUlid() + val ulid: Ulid = + if (generateMonotonicUlid.get()) { + if (useIndividualTime.get()) { + UlidCreator.getMonotonicUlid(individualTime.get()) + } else { + UlidCreator.getMonotonicUlid() + } + } else { + if (useIndividualTime.get()) { + UlidCreator.getUlid(individualTime.get()) + } else { + UlidCreator.getUlid() + } } - } - else { - if (useIndividualTime.get()) { - UlidCreator.getUlid(individualTime.get()) - } - else { - UlidCreator.getUlid() - } - } return ulidFormat.get().format(ulid) } - override fun getData(dataId: String): Any? = when { - parsedUlIdTimestampDataKey.`is`(dataId) -> StringUtil.stripHtml(parsedUlIdTimestamp.get(), false) - parsedUlIdTransformedDataKey.`is`(dataId) -> StringUtil.stripHtml(parsedUlIdTransformed.get(), false) - else -> super.getData(dataId) - } + override fun getData(dataId: String): Any? = + when { + parsedUlIdTimestampDataKey.`is`(dataId) -> + StringUtil.stripHtml(parsedUlIdTimestamp.get(), false) + parsedUlIdTransformedDataKey.`is`(dataId) -> + StringUtil.stripHtml(parsedUlIdTransformed.get(), false) + else -> super.getData(dataId) + } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun parseUlid() { val parseUlidInputValue = parseUlidInput.get() @@ -157,11 +151,15 @@ class UlidGenerator( } val ulid = Ulid.from(parseUlidInputValue) - parsedUlIdTimestamp.set("${ulid.time} (${DateTimeFormatter.ISO_INSTANT.format(ulid.instant)})") - parsedUlIdTransformed.set("${parseUlidTransformFormat.get().format(ulid)}") + parsedUlIdTimestamp.set( + "${ulid.time} (${DateTimeFormatter.ISO_INSTANT.format(ulid.instant)})" + ) + parsedUlIdTransformed.set( + "${parseUlidTransformFormat.get().format(ulid)}" + ) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class UlidFormat(val title: String, val format: (Ulid) -> String) { @@ -173,29 +171,27 @@ class UlidGenerator( override fun toString(): String = title } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "ULID", - contentTitle = "ULID Generator" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "ULID", contentTitle = "ULID Generator") override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> UlidGenerator) = { configuration -> UlidGenerator(project, context, configuration, parentDisposable) } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { private val parsedUlIdTimestampDataKey = DataKey.create("parsedUlIdTimestamp") private val parsedUlIdTransformedDataKey = DataKey.create("parsedUlIdTransformed") } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/MacAddressBasedUuidGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/MacAddressBasedUuidGenerator.kt new file mode 100644 index 00000000..de2f3cbb --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/MacAddressBasedUuidGenerator.kt @@ -0,0 +1,159 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid + +import com.fasterxml.uuid.EthernetAddress +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.ui.asSequence +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.selected +import com.intellij.ui.dsl.builder.whenItemSelectedFromUi +import com.intellij.ui.layout.ComponentPredicate +import com.intellij.ui.layout.ValidationInfoBuilder +import dev.turingcomplete.intellijdevelopertoolsplugin.common.toHexMacAddress +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bind +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid.MacAddressBasedUuidGenerator.MacAddressGenerationMode.INDIVIDUAL +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid.MacAddressBasedUuidGenerator.MacAddressGenerationMode.LOCAL_INTERFACE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid.MacAddressBasedUuidGenerator.MacAddressGenerationMode.RANDOM +import java.net.NetworkInterface +import java.net.SocketException + +abstract class MacAddressBasedUuidGenerator( + version: UuidVersion, + configuration: DeveloperToolConfiguration, + supportsBulkGeneration: Boolean, +) : SpecificUuidGenerator(supportsBulkGeneration) { + // -- Properties ---------------------------------------------------------- // + + private var macAddressGenerationMode = + configuration.register("${version}MacAddressGenerationMode", RANDOM) + private var localInterface by configuration.register("${version}LocalInterface", "") + private var individualMacAddress = configuration.register("${version}IndividualMacAddress", "") + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + @Suppress("UnstableApiUsage") + override fun Panel.buildConfigurationUi(visible: ComponentPredicate) { + buttonsGroup("MAC address:") { + row { + radioButton("Generate random multicast MAC address") + .bind(macAddressGenerationMode, RANDOM) + } + + row { + val individualRadioButton = + radioButton("Individual:") + .bind(macAddressGenerationMode, INDIVIDUAL) + .gap(RightGap.SMALL) + expandableTextField() + .bindText(individualMacAddress) + .validationInfo(validateIndividualMacAddress()) + .enabledIf(individualRadioButton.selected) + .component + } + + row { + val localMacAddresses = collectLocalMacAddresses() + visible(localMacAddresses.isNotEmpty()) + val useLocalInterface = + radioButton("Local interface:") + .bind(macAddressGenerationMode, LOCAL_INTERFACE) + .gap(RightGap.SMALL) + comboBox(localMacAddresses) + .applyToComponent { + model + .asSequence() + .firstOrNull { it.macAddress == localInterface } + ?.let { selectedItem = it } + } + .whenItemSelectedFromUi { localInterface = it.macAddress } + .enabledIf(useLocalInterface.selected) + .component + } + } + .visibleIf(visible) + } + + fun getEthernetAddress(): EthernetAddress = + when (macAddressGenerationMode.get()) { + RANDOM -> EthernetAddress.constructMulticastAddress() + INDIVIDUAL -> EthernetAddress.valueOf(individualMacAddress.get()) + LOCAL_INTERFACE -> EthernetAddress(localInterface) + } + + // -- Private Methods ----------------------------------------------------- // + + private fun validateIndividualMacAddress(): + ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = { + if ( + macAddressGenerationMode.get() == INDIVIDUAL && + !MAC_ADDRESS_REGEX.matches(individualMacAddress.get()) + ) { + INVALID_MAC_ADDRESS_VALIDATION_INFO + } else { + null + } + } + + private fun collectLocalMacAddresses(): List { + return try { + NetworkInterface.getNetworkInterfaces() + .asSequence() + .filter { !it.isLoopback } + .filter { it.hardwareAddress != null } + .filter { it.hardwareAddress.size == 6 } + .groupBy { it.hardwareAddress.toHexMacAddress() } + .map { + LocalInterface( + it.key, + it.value.joinToString("/") { networkInterface -> networkInterface.name }, + ) + } + .toList() + } catch (_: SocketException) { + emptyList() + } + } + + // -- Inner Type ---------------------------------------------------------- // + + enum class MacAddressGenerationMode { + + RANDOM, + INDIVIDUAL, + LOCAL_INTERFACE, + } + + // -- Inner Type ---------------------------------------------------------- // + + private class LocalInterface(val macAddress: String, val title: String) { + + override fun toString(): String = "$title (${macAddress})" + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (javaClass != other?.javaClass) { + return false + } + + other as LocalInterface + + return macAddress == other.macAddress + } + + override fun hashCode(): Int = macAddress.hashCode() + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val MAC_ADDRESS_REGEX = Regex("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$") + private val INVALID_MAC_ADDRESS_VALIDATION_INFO = ValidationInfo("Must be a valid MAC address") + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/NamespaceAndNameBasedUuidGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/NamespaceAndNameBasedUuidGenerator.kt new file mode 100644 index 00000000..b1c818bc --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/NamespaceAndNameBasedUuidGenerator.kt @@ -0,0 +1,120 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid + +import com.fasterxml.uuid.Generators +import com.intellij.openapi.Disposable +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.selected +import com.intellij.ui.dsl.builder.text +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import com.intellij.ui.layout.ComponentPredicate +import com.intellij.ui.layout.ValidationInfoBuilder +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bind +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid.NamespaceAndNameBasedUuidGenerator.NamespaceMode.INDIVIDUAL +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid.NamespaceAndNameBasedUuidGenerator.NamespaceMode.PREDEFINED +import java.security.MessageDigest +import java.util.UUID + +abstract class NamespaceAndNameBasedUuidGenerator( + version: UuidVersion, + private val algorithm: MessageDigest, + configuration: DeveloperToolConfiguration, + private val parentDisposable: Disposable, + supportsBulkGeneration: Boolean, +) : SpecificUuidGenerator(supportsBulkGeneration) { + // -- Properties ---------------------------------------------------------- // + + private var namespaceMode = configuration.register("${version}NamespaceMode", PREDEFINED) + private var predefinedNamespace = + configuration.register("${version}PredefinedNamespace", PredefinedNamespace.DNS) + private var individualNamespace = configuration.register("${version}IndividualNamespace", "") + private var name = configuration.register("${version}Name", "") + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + @Suppress("UnstableApiUsage") + override fun Panel.buildConfigurationUi(visible: ComponentPredicate) { + rowsRange { + buttonsGroup("Namespace:") { + row { + val usePredefined = + radioButton("Predefined:").bind(namespaceMode, PREDEFINED).gap(RightGap.SMALL) + comboBox(PredefinedNamespace.entries) + .bindItem(predefinedNamespace) + .enabledIf(usePredefined.selected) + .component + } + + row { + val individualRadioButton = + radioButton("Individual:").bind(namespaceMode, INDIVIDUAL).gap(RightGap.SMALL) + textField() + .text(individualNamespace.get()) + .validationInfo(validateIndividualNamespace()) + .whenTextChangedFromUi(parentDisposable) { individualNamespace.set(it) } + .enabledIf(individualRadioButton.selected) + .component + } + } + + row { expandableTextField().label("Name:").bindText(name) } + } + .visibleIf(visible) + } + + private fun validateIndividualNamespace(): + ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = { + if (namespaceMode.get() == INDIVIDUAL && !UUID_REGEX.matches(it.text)) { + ValidationInfo("Must be a valid UUID") + } else { + null + } + } + + final override fun generate(): String { + val namespace: UUID = + when (namespaceMode.get()) { + PREDEFINED -> predefinedNamespace.get().value + INDIVIDUAL -> UUID.fromString(individualNamespace.get()) + } + + return Generators.nameBasedGenerator(namespace, algorithm).generate(name.get()).toString() + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + private enum class NamespaceMode { + + PREDEFINED, + INDIVIDUAL, + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class PredefinedNamespace(private val title: String, val value: UUID) { + + DNS("DNS", UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")), + URL("URL", UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8")), + OID("OID", UUID.fromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8")), + X500DN("X.500 DN", UUID.fromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8")); + + override fun toString(): String = "$title ($value)" + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val UUID_REGEX = + Regex( + "^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}\$" + ) + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/SpecificUuidGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/SpecificUuidGenerator.kt similarity index 61% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/SpecificUuidGenerator.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/SpecificUuidGenerator.kt index a44f4c70..7dbb1c39 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/SpecificUuidGenerator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/SpecificUuidGenerator.kt @@ -1,12 +1,12 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.layout.ComponentPredicate abstract class SpecificUuidGenerator(val supportsBulkGeneration: Boolean) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // abstract fun generate(): String @@ -14,7 +14,7 @@ abstract class SpecificUuidGenerator(val supportsBulkGeneration: Boolean) { // Override if needed } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/UuidGenerator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/UuidGenerator.kt similarity index 59% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/UuidGenerator.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/UuidGenerator.kt index 0860e947..ead6f324 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/UuidGenerator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/UuidGenerator.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid import com.fasterxml.uuid.Generators import com.intellij.openapi.Disposable @@ -7,25 +7,26 @@ import com.intellij.openapi.ui.ComboBox import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.layout.ComboBoxPredicate -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.OneLineTextGenerator import dev.turingcomplete.intellijdevelopertoolsplugin.common.toMessageDigest -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.OneLineTextGenerator -internal class UuidGenerator( +class UuidGenerator( project: Project?, context: DeveloperUiToolContext, configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : OneLineTextGenerator( - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // + parentDisposable: Disposable, +) : + OneLineTextGenerator( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + ) { + // -- Properties ---------------------------------------------------------- // private var selectedUuidVersion = configuration.register("version", UuidVersion.V4) @@ -36,21 +37,19 @@ internal class UuidGenerator( private val uuidV6Generator by lazy { UuidV6Generator(configuration) } private val uuidV7Generator by lazy { UuidV7Generator() } - // -- Initialization ---------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // init { selectedUuidVersion.afterChange(parentDisposable) { handleVersionSelection() } } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // override fun Panel.buildConfigurationUi() { lateinit var selectedVersionComboBox: ComboBox row { - selectedVersionComboBox = comboBox(UuidVersion.entries) - .label("Version:") - .bindItem(selectedUuidVersion) - .component + selectedVersionComboBox = + comboBox(UuidVersion.entries).label("Version:").bindItem(selectedUuidVersion).component } with(uuidV1Generator) { @@ -77,14 +76,17 @@ internal class UuidGenerator( override fun generate(): String = getGeneratorForSelectedUuidVersion().generate() - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun handleVersionSelection() { val uuidVersion = selectedUuidVersion.get() - supportsBulkGeneration.value = getGeneratorForSelectedUuidVersion(uuidVersion).supportsBulkGeneration + supportsBulkGeneration.value = + getGeneratorForSelectedUuidVersion(uuidVersion).supportsBulkGeneration } - private fun getGeneratorForSelectedUuidVersion(version: UuidVersion = selectedUuidVersion.get()): SpecificUuidGenerator = + private fun getGeneratorForSelectedUuidVersion( + version: UuidVersion = selectedUuidVersion.get() + ): SpecificUuidGenerator = when (version) { UuidVersion.V1 -> uuidV1Generator UuidVersion.V3 -> uuidV3Generator @@ -94,69 +96,81 @@ internal class UuidGenerator( UuidVersion.V7 -> uuidV7Generator } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class UuidV1Generator( - configuration: DeveloperToolConfiguration - ) : MacAddressBasedUuidGenerator(UuidVersion.V1, configuration, true) { + private class UuidV1Generator(configuration: DeveloperToolConfiguration) : + MacAddressBasedUuidGenerator(UuidVersion.V1, configuration, true) { - override fun generate(): String = Generators.timeBasedGenerator(getEthernetAddress()).generate().toString() + override fun generate(): String = + Generators.timeBasedGenerator(getEthernetAddress()).generate().toString() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class UuidV3Generator( configuration: DeveloperToolConfiguration, - parentDisposable: Disposable - ) : NamespaceAndNameBasedUuidGenerator(UuidVersion.V3, "MD5".toMessageDigest(), configuration, parentDisposable, false) + parentDisposable: Disposable, + ) : + NamespaceAndNameBasedUuidGenerator( + UuidVersion.V3, + "MD5".toMessageDigest(), + configuration, + parentDisposable, + false, + ) - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class UuidV4Generator : SpecificUuidGenerator(true) { override fun generate(): String = Generators.randomBasedGenerator().generate().toString() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class UuidV5Generator( configuration: DeveloperToolConfiguration, - parentDisposable: Disposable - ) : NamespaceAndNameBasedUuidGenerator(UuidVersion.V5, "SHA-1".toMessageDigest(), configuration, parentDisposable, false) + parentDisposable: Disposable, + ) : + NamespaceAndNameBasedUuidGenerator( + UuidVersion.V5, + "SHA-1".toMessageDigest(), + configuration, + parentDisposable, + false, + ) - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private class UuidV6Generator( - configuration: DeveloperToolConfiguration - ) : MacAddressBasedUuidGenerator(UuidVersion.V6, configuration, true) { + private class UuidV6Generator(configuration: DeveloperToolConfiguration) : + MacAddressBasedUuidGenerator(UuidVersion.V6, configuration, true) { - override fun generate(): String = Generators.timeBasedReorderedGenerator(getEthernetAddress()).generate().toString() + override fun generate(): String = + Generators.timeBasedReorderedGenerator(getEthernetAddress()).generate().toString() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class UuidV7Generator : SpecificUuidGenerator(true) { override fun generate(): String = Generators.timeBasedEpochGenerator().generate().toString() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "UUID", - contentTitle = "UUID Generator" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "UUID", contentTitle = "UUID Generator") override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> UuidGenerator) = { configuration -> UuidGenerator(project, context, configuration, parentDisposable) } } - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/UuidVersion.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/UuidVersion.kt similarity index 56% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/UuidVersion.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/UuidVersion.kt index 48a0f0ad..1ed66d84 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/UuidVersion.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/generator/uuid/UuidVersion.kt @@ -1,7 +1,7 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.uuid enum class UuidVersion(val title: String) { - // -- Values ------------------------------------------------------------------------------------------------------ // + // -- Values -------------------------------------------------------------- // V1("UUIDv1"), V3("UUIDv3"), @@ -12,10 +12,10 @@ enum class UuidVersion(val title: String) { override fun toString(): String = title - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/GeneralBundle.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/message/GeneralBundle.kt similarity index 61% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/GeneralBundle.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/message/GeneralBundle.kt index 0f4b9fe9..056e403c 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/GeneralBundle.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/message/GeneralBundle.kt @@ -1,21 +1,21 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.message +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message import com.intellij.DynamicBundle import org.jetbrains.annotations.PropertyKey object GeneralBundle { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // - const val ID = "messages.GeneralBundle" + private const val ID = "message.GeneralBundle" private val instance: DynamicBundle = DynamicBundle(GeneralBundle::class.java, ID) - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun message(@PropertyKey(resourceBundle = ID) key: String, vararg params: Any): String = instance.getMessage(key, *params) - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // } diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/UiToolsBundle.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/message/UiToolsBundle.kt similarity index 61% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/UiToolsBundle.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/message/UiToolsBundle.kt index 09f3a8a0..04f7f81b 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/UiToolsBundle.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/message/UiToolsBundle.kt @@ -1,21 +1,21 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.message +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message import com.intellij.DynamicBundle import org.jetbrains.annotations.PropertyKey object UiToolsBundle { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // - private const val ID = "messages.UiToolsBundle" + private const val ID = "message.UiToolsBundle" private val instance: DynamicBundle = DynamicBundle(UiToolsBundle::class.java, ID) - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // fun message(@PropertyKey(resourceBundle = ID) key: String, vararg params: Any): String = instance.getMessage(key, *params) - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/AsciiArtCreator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/AsciiArtCreator.kt similarity index 59% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/AsciiArtCreator.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/AsciiArtCreator.kt index dc94a813..aefc7513 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/AsciiArtCreator.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/AsciiArtCreator.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other import com.github.lalyos.jfiglet.FigletFont import com.intellij.openapi.Disposable @@ -17,22 +17,21 @@ import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.builder.whenItemSelectedFromUi import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.AsyncTaskExecutor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor.EditorMode.OUTPUT -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.GitHubUtils -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.hyperLink -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.message.UiToolsBundle +import dev.turingcomplete.intellijdevelopertoolsplugin.common.GitHubUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty import dev.turingcomplete.intellijdevelopertoolsplugin.common.clearDirectory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AsyncTaskExecutor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.hyperLink +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle import java.io.InputStream import java.nio.file.Files import java.nio.file.Paths @@ -43,14 +42,15 @@ class AsciiArtCreator( private val configuration: DeveloperToolConfiguration, private val project: Project?, parentDisposable: Disposable, - private val context: DeveloperUiToolContext + private val context: DeveloperUiToolContext, ) : DeveloperUiTool(parentDisposable = parentDisposable), DataProvider { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val log = logger() private val textInput = configuration.register("textInput", "", INPUT, "Awesome") - private val selectedFontFileName = configuration.register("selectedFontFileName", DEFAULT_BUILT_IN_FILE_NAME, CONFIGURATION) + private val selectedFontFileName = + configuration.register("selectedFontFileName", DEFAULT_BUILT_IN_FILE_NAME, CONFIGURATION) private val asciiArtOutputErrorHolder = ErrorHolder() private val asciiArtOutput = ValueProperty("") @@ -61,8 +61,8 @@ class AsciiArtCreator( private val syncFontsAlarm by lazy { AsyncTaskExecutor.onEdt(parentDisposable) } private val createAsciiArtAlarm by lazy { AsyncTaskExecutor.onEdt(parentDisposable) } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // @Suppress("UnstableApiUsage") override fun Panel.buildUi() { @@ -83,32 +83,44 @@ class AsciiArtCreator( .whenItemSelectedFromUi { createAsciiArt() } .gap(RightGap.SMALL) .applyToComponent { prototypeDisplayValue = "x".repeat(30) } - hyperLink(UiToolsBundle.message("ascii-art.examples"), "https://github.com/xero/figlet-fonts/blob/master/Examples.md") + hyperLink( + UiToolsBundle.message("ascii-art.examples"), + "https://github.com/xero/figlet-fonts/blob/master/Examples.md", + ) } row { - val editor = DeveloperToolEditor( - id = "asciiArtOutput", - context = context, - configuration = configuration, - project = project, - title = UiToolsBundle.message("ascii-art.output-title"), - editorMode = OUTPUT, - parentDisposable = parentDisposable, - textProperty = asciiArtOutput, - fixedEditorSoftWraps = false - ).onTextChangeFromUi { createAsciiArt() } - cell(editor.component) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .validationOnApply(editor.bindValidator(asciiArtOutputErrorHolder.asValidation())) - .resizableColumn().align(Align.FILL) - }.resizableRow().topGap(TopGap.MEDIUM).bottomGap(BottomGap.MEDIUM) + val editor = + AdvancedEditor( + id = "asciiArtOutput", + context = context, + configuration = configuration, + project = project, + title = UiToolsBundle.message("ascii-art.output-title"), + editorMode = AdvancedEditor.EditorMode.OUTPUT, + parentDisposable = parentDisposable, + textProperty = asciiArtOutput, + fixedEditorSoftWraps = false, + ) + .onTextChangeFromUi { createAsciiArt() } + cell(editor.component) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .validationOnApply(editor.bindValidator(asciiArtOutputErrorHolder.asValidation())) + .resizableColumn() + .align(Align.FILL) + } + .resizableRow() + .topGap(TopGap.MEDIUM) + .bottomGap(BottomGap.MEDIUM) row { lateinit var downloadFontsButton: JButton - downloadFontsButton = button(UiToolsBundle.message("ascii-art.download-additional-ascii-art-fonts")) { - downloadAdditionalAsciiArtFonts(downloadFontsButton) - }.gap(RightGap.SMALL).component + downloadFontsButton = + button(UiToolsBundle.message("ascii-art.download-additional-ascii-art-fonts")) { + downloadAdditionalAsciiArtFonts(downloadFontsButton) + } + .gap(RightGap.SMALL) + .component contextHelp(UiToolsBundle.message("ascii-art.download-additional-ascii-art-fonts-help")) } } @@ -117,7 +129,11 @@ class AsciiArtCreator( syncFonts() } - // -- Private Methods --------------------------------------------------------------------------------------------- // + override fun reset() { + createAsciiArt() + } + + // -- Private Methods ----------------------------------------------------- // private fun downloadAdditionalAsciiArtFonts(downloadFontsButton: JButton) { ApplicationManager.getApplication().executeOnPooledThread { @@ -126,40 +142,44 @@ class AsciiArtCreator( } catch (e: Exception) { log.warn("Failed to clear ASCII art fonts download directory: $downloadedFontsPath", e) } - ApplicationManager.getApplication().invokeLater { - syncFonts() - } + ApplicationManager.getApplication().invokeLater { syncFonts() } GitHubUtils.downloadFiles( - project = project!!, - repositoryUrl = "https://github.com/xero/figlet-fonts", - destinationPath = downloadedFontsPath, - preDownloadFilter = { fileName -> fileName.endsWith(".flf") }, - afterDownloadFilter = { file -> - try { - createAsciiArt(Files.newInputStream(file), UiToolsBundle.message("ascii-art.example")) - true - } catch (e: Exception) { - log.warn("Failed to render example ASCII art using font file: $file. Font file will be ignored.", e) - false - } - }, - onStart = { downloadFontsButton.isEnabled = false }, - onSuccess = { }, - onThrowable = { - ApplicationManager.getApplication().invokeLater { - Messages.showErrorDialog( - project, - UiToolsBundle.message("ascii-art.download-additional-ascii-art-fonts-failed-details"), - UiToolsBundle.message("ascii-art.download-additional-ascii-art-fonts-failed-title"), - ) - } - }, - onFinished = { - syncFonts() - downloadFontsButton.isEnabled = true - } - ).queue() + project = project!!, + repositoryUrl = "https://github.com/xero/figlet-fonts", + destinationPath = downloadedFontsPath, + preDownloadFilter = { fileName -> fileName.endsWith(".flf") }, + afterDownloadFilter = { file -> + try { + createAsciiArt(Files.newInputStream(file), UiToolsBundle.message("ascii-art.example")) + true + } catch (e: Exception) { + log.warn( + "Failed to render example ASCII art using font file: $file. Font file will be ignored.", + e, + ) + false + } + }, + onStart = { downloadFontsButton.isEnabled = false }, + onSuccess = {}, + onThrowable = { + ApplicationManager.getApplication().invokeLater { + Messages.showErrorDialog( + project, + UiToolsBundle.message( + "ascii-art.download-additional-ascii-art-fonts-failed-details" + ), + UiToolsBundle.message("ascii-art.download-additional-ascii-art-fonts-failed-title"), + ) + } + }, + onFinished = { + syncFonts() + downloadFontsButton.isEnabled = true + }, + ) + .queue() } } @@ -209,8 +229,7 @@ class AsciiArtCreator( val fontResource = getBuiltInFontResource(it) if (fontResource != null) { fontResources.put(it) { getBuiltInFontResource(it)!! } - } - else { + } else { log.warn("Built-in font $it not found") } } @@ -223,12 +242,13 @@ class AsciiArtCreator( this.fontResources.clear() this.fontResources.putAll(fontResources) - fontFileNamesComboBoxModel.setFileNames(fontResources.keys) if (!fontResources.containsKey(selectedFontFileName.get())) { selectedFontFileName.set(DEFAULT_BUILT_IN_FILE_NAME) } + fontFileNamesComboBoxModel.setFileNames(fontResources.keys, selectedFontFileName.get()) + createAsciiArt() } syncFontsAlarm.enqueueTask(request, 0) @@ -237,7 +257,7 @@ class AsciiArtCreator( private fun getBuiltInFontResource(fontFileName: String): InputStream? = FigletFont::class.java.getClassLoader().getResourceAsStream(fontFileName) - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class FontFileNamesComboBoxModel() : DefaultComboBoxModel() { @@ -247,51 +267,50 @@ class AsciiArtCreator( override fun getElementAt(index: Int): String? = fontFileNames[index] - fun setFileNames(fileNames: Collection) { + fun setFileNames(fileNames: Collection, selectedFontFileName: String) { fontFileNames.clear() fontFileNames.addAll(fileNames.sorted()) if (fileNames.isNotEmpty()) { // This will also call `fireContentsChanged` - selectedItem = fileNames.first() - } - else { + selectedItem = selectedFontFileName + } else { fireContentsChanged(this, -1, -1) } } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( menuTitle = UiToolsBundle.message("ascii-art.menu-title"), - contentTitle = UiToolsBundle.message("ascii-art.content-title") + contentTitle = UiToolsBundle.message("ascii-art.content-title"), ) override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> AsciiArtCreator) = - { configuration -> - AsciiArtCreator( - configuration = configuration, - project = project, - parentDisposable = parentDisposable, - context = context - ) - } + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> AsciiArtCreator) = { configuration -> + AsciiArtCreator( + configuration = configuration, + project = project, + parentDisposable = parentDisposable, + context = context, + ) + } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { const val DEFAULT_BUILT_IN_FILE_NAME = "standard.flf" private val builtInFonts = listOf(DEFAULT_BUILT_IN_FILE_NAME, "slant.flf") - private val downloadedFontsPath = PathManager.getSystemDir().resolve(Paths.get("plugins", "developer-tools", "ascii-fonts")) + private val downloadedFontsPath = + PathManager.getSystemDir().resolve(Paths.get("plugins", "developer-tools", "ascii-fonts")) } } diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ColorPicker.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ColorPicker.kt new file mode 100644 index 00000000..d2248dee --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ColorPicker.kt @@ -0,0 +1,346 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.DataKey +import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.observable.properties.AtomicProperty +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.JBColor +import com.intellij.ui.colorpicker.ColorPickerBuilder +import com.intellij.ui.colorpicker.ColorPickerModel +import com.intellij.ui.colorpicker.MaterialGraphicalColorPipetteProvider +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.COLUMNS_TINY +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.actionButton +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import com.intellij.util.ui.JBUI +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.CopyAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.NotBlankInputValidator +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindIntTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.toJBColor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import java.awt.Color +import java.util.Locale +import javax.swing.border.LineBorder + +class ColorPicker( + private val project: Project?, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, +) : DeveloperUiTool(parentDisposable), DataProvider { + // -- Properties ---------------------------------------------------------- // + + private val selectedColor: ValueProperty = + configuration.register("selectedColor", JBColor.MAGENTA.toJBColor(), INPUT) + + private val decimalPlaces: ValueProperty = + configuration.register("decimalPlaces", 2, CONFIGURATION) + + private val cssRgb = AtomicProperty("") + private val cssRgbWithAlpha = AtomicProperty("") + private val cssHex = AtomicProperty("") + private val cssHexWithAlpha = AtomicProperty("") + private val cssHls = AtomicProperty("") + private val cssHlsWithAlpha = AtomicProperty("") + + private lateinit var colorPickerModel: ColorPickerModel + + // -- Initialization ------------------------------------------------------ // + + init { + selectedColor.afterChangeConsumeEvent(parentDisposable) { + setCssValues() + if (it.id?.equals(COLOR_SELECTION_CHANGE_ID) != true) { + colorPickerModel.setColor(it.newValue) + } + } + } + + // -- Exported Methods ---------------------------------------------------- // + + override fun Panel.buildUi() { + row { + val colorPicker = + ColorPickerBuilder(showAlpha = true, showAlphaAsPercent = true) + .setOriginalColor(selectedColor.get()) + .withFocus() + .addSaturationBrightnessComponent() + .addColorAdjustPanel(MaterialGraphicalColorPipetteProvider()) + .addColorValuePanel() + .addColorListener( + { color, _ -> selectedColor.set(color.toJBColor(), COLOR_SELECTION_CHANGE_ID) }, + true, + ) + .apply { colorPickerModel = this.model } + .build() + .apply { + content.apply { + border = LineBorder(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()) + } + } + cell(colorPicker.content).align(Align.FILL) + } + .bottomGap(BottomGap.MEDIUM) + + group(UiToolsBundle.message("color-picker.css-colors.title")) { + row { + label("").bindText(cssRgb).gap(RightGap.SMALL) + + actionButton(CopyAction(cssRgbDataKey), ColorPicker::class.java.name) + } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + row { + label("").bindText(cssRgbWithAlpha).gap(RightGap.SMALL) + actionButton(CopyAction(cssRgbWithAlphaDataKey), ColorPicker::class.java.name) + } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + row { + label("").bindText(cssHex).gap(RightGap.SMALL) + actionButton(CopyAction(cssHexDataKey), ColorPicker::class.java.name) + } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + row { + label("").bindText(cssHexWithAlpha).gap(RightGap.SMALL) + actionButton(CopyAction(cssHexWithAlphaDataKey), ColorPicker::class.java.name) + } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + row { + label("").bindText(cssHls).gap(RightGap.SMALL) + actionButton(CopyAction(cssHslDataKey), ColorPicker::class.java.name) + } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + row { + label("").bindText(cssHlsWithAlpha).gap(RightGap.SMALL) + actionButton(CopyAction(cssHslWithAlphaDataKey), ColorPicker::class.java.name) + } + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + row { + button(UiToolsBundle.message("color-picker.parse-css-color-action.title")) { + val inputDialog = + Messages.InputDialog( + project, + UiToolsBundle.message("color-picker.parse-css-color-action.input"), + UiToolsBundle.message("color-picker.parse-css-color-action.title"), + null, + "", + NotBlankInputValidator(), + ) + inputDialog.show() + inputDialog.inputString?.let { parseCssColorValue(it) }?.let { selectedColor.set(it) } + } + } + .topGap(TopGap.NONE) + } + + collapsibleGroup(UiToolsBundle.message("color-picker.settings.title")) { + row { + textField() + .label(UiToolsBundle.message("color-picker.settings.decimal-places")) + .bindIntTextImproved(decimalPlaces) + .validateLongValue(LongRange(1, 4)) + .columns(COLUMNS_TINY) + .whenTextChangedFromUi { setCssValues() } + } + } + } + + override fun afterBuildUi() { + setCssValues() + } + + override fun getData(dataId: String): Any? = + when { + cssRgbDataKey.`is`(dataId) -> StringUtil.stripHtml(cssRgb.get(), false) + cssRgbWithAlphaDataKey.`is`(dataId) -> StringUtil.stripHtml(cssRgbWithAlpha.get(), false) + cssHexDataKey.`is`(dataId) -> StringUtil.stripHtml(cssHex.get(), false) + cssHexWithAlphaDataKey.`is`(dataId) -> StringUtil.stripHtml(cssHexWithAlpha.get(), false) + cssHslDataKey.`is`(dataId) -> StringUtil.stripHtml(cssHls.get(), false) + cssHslWithAlphaDataKey.`is`(dataId) -> StringUtil.stripHtml(cssHlsWithAlpha.get(), false) + else -> null + } + + fun createCssValues(color: Color): CssValues { + val red = color.red + val green = color.green + val blue = color.blue + val alpha = color.alpha + val hsl = rgbToHsl(red, green, blue) + return CssValues( + rgb = formatCssRgb(red = red, green = green, blue = blue), + rgbWithAlpha = formatCssRgb(red = red, green = green, blue = blue, alpha = alpha), + hex = "#%02X%02X%02X".format(Locale.US, red, green, blue), + hexWithAlpha = "#%02X%02X%02X%02X".format(Locale.US, red, green, blue, alpha), + hls = formatCssHsl(hsl), + hlsWithAlpha = formatCssHsl(hsl, alpha), + ) + } + + // -- Private Methods ----------------------------------------------------- // + + private fun parseCssColorValue(inputString: String): JBColor? = + try { + val color = org.silentsoft.csscolor4j.Color.valueOf(inputString) + JBColor( + Color(color.red, color.green, color.blue, (color.opacity * 255.0).toInt()), + Color(color.red, color.green, color.blue, (color.opacity * 255.0).toInt()), + ) + } catch (e: IllegalArgumentException) { + Messages.showErrorDialog( + project, + e.message, + UiToolsBundle.message("color-picker.parse-css-color-action.title"), + ) + null + } catch (_: Exception) { + Messages.showErrorDialog( + project, + UiToolsBundle.message("color-picker.parse-css-color-action.error.message"), + UiToolsBundle.message("color-picker.parse-css-color-action.title"), + ) + null + } + + private fun setCssValues() { + setCssValues(createCssValues(selectedColor.get())) + } + + private fun setCssValues(cssValues: CssValues) { + fun String.formatHtml(): String = "${this}" + + cssRgb.set(cssValues.rgb.formatHtml()) + cssRgbWithAlpha.set(cssValues.rgbWithAlpha.formatHtml()) + cssHex.set(cssValues.hex.formatHtml()) + cssHexWithAlpha.set(cssValues.hexWithAlpha.formatHtml()) + cssHls.set(cssValues.hls.formatHtml()) + cssHlsWithAlpha.set(cssValues.hlsWithAlpha.formatHtml()) + } + + private fun rgbToHsl(r: Int, g: Int, b: Int): Triple { + val rNorm = r / 255f + val gNorm = g / 255f + val bNorm = b / 255f + + val max = maxOf(rNorm, gNorm, bNorm) + val min = minOf(rNorm, gNorm, bNorm) + val l = (max + min) / 2f + + if (max == min) { + return Triple(0f, 0f, l * 100) // achromatic + } + + val d = max - min + val s = if (l > 0.5f) d / (2f - max - min) else d / (max + min) + + val h = + when (max) { + rNorm -> ((gNorm - bNorm) / d + if (gNorm < bNorm) 6 else 0) + gNorm -> ((bNorm - rNorm) / d + 2) + else -> ((rNorm - gNorm) / d + 4) + } / 6f + + return Triple(h * 360, s * 100, l * 100) + } + + private fun formatCssRgb(red: Int, green: Int, blue: Int, alpha: Int? = null): String { + return if (alpha != null) { + val alphaNormalized = alpha.coerceIn(0, 255) / 255f + val aStr = formatNumber(alphaNormalized) + "rgba($red, $green, $blue, $aStr)" + } else { + "rgb($red, $green, $blue)" + } + } + + private fun formatCssHsl(hsl: Triple, alpha: Int? = null): String { + val (h, s, l) = hsl + val hStr = formatNumber(h) + val sStr = formatNumber(s) + "%" + val lStr = formatNumber(l) + "%" + + return if (alpha != null) { + val alphaNormalized = alpha.coerceIn(0, 255) / 255f + val aStr = formatNumber(alphaNormalized) + "hsla($hStr, $sStr, $lStr, $aStr)" + } else { + "hsl($hStr, $sStr, $lStr)" + } + } + + fun formatNumber(value: Float): String { + val rounded = String.format(Locale.US, "%.${decimalPlaces.get()}f", value) + return rounded.trimEnd('0').trimEnd('.') + } + + // -- Inner Type ---------------------------------------------------------- // + + data class CssValues( + val rgb: String, + val rgbWithAlpha: String, + val hex: String, + val hexWithAlpha: String, + val hls: String, + val hlsWithAlpha: String, + ) + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("color-picker.title"), + contentTitle = UiToolsBundle.message("color-picker.content-title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> ColorPicker) = { configuration -> + ColorPicker(project, configuration, parentDisposable) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val COLOR_SELECTION_CHANGE_ID = "colorSelection" + + private val cssRgbDataKey = DataKey.create("cssRgb") + private val cssRgbWithAlphaDataKey = DataKey.create("cssRgbWithAlpha") + private val cssHexDataKey = DataKey.create("cssHex") + private val cssHexWithAlphaDataKey = DataKey.create("cssHexWithAlpha") + private val cssHslDataKey = DataKey.create("cssHsl") + private val cssHslWithAlphaDataKey = DataKey.create("cssHslWithAlpha") + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/IntelliJInternals.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/IntelliJInternals.kt new file mode 100644 index 00000000..0ffb114d --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/IntelliJInternals.kt @@ -0,0 +1,355 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.intellij.ide.BrowserUtil +import com.intellij.ide.plugins.IdeaPluginDescriptor +import com.intellij.ide.plugins.PluginManager +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.extensions.PluginDescriptor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.setEmptyState +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.TableSpeedSearch +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.table.JBTable +import com.intellij.ui.treeStructure.Tree +import com.intellij.util.lang.UrlClassLoader +import com.intellij.util.ui.ColumnInfo +import com.intellij.util.ui.ListTableModel +import dev.turingcomplete.intellijdevelopertoolsplugin.common.CopyValuesAction +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginCommonDataKeys +import dev.turingcomplete.intellijdevelopertoolsplugin.common.extension +import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.simpleColumnInfo +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.setContextMenu +import java.awt.Dimension +import java.net.URLClassLoader +import java.nio.file.Files +import java.nio.file.Paths +import javax.swing.ListSelectionModel +import javax.swing.tree.DefaultMutableTreeNode +import kotlin.streams.asSequence + +class IntelliJInternals(parentDisposable: Disposable, private val project: Project?) : + DeveloperUiTool(parentDisposable = parentDisposable), DataProvider { + // -- Properties ---------------------------------------------------------- // + + private lateinit var pluginOverviewTable: JBTable + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun Panel.buildUi() { + group("Plugins") { + row { + val pluginOverviewTableModel = + ListTableModel(*pluginOverviewTableColumns).apply { + isSortable = true + } + pluginOverviewTable = + JBTable(pluginOverviewTableModel).apply { + setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) + rowSelectionAllowed = true + columnSelectionAllowed = false + setContextMenu( + this::class.java.name, + DefaultActionGroup( + CopyValuesAction( + valueToString = { + val ideaPluginDescriptor = it as IdeaPluginDescriptor + "${ideaPluginDescriptor.name} (${ideaPluginDescriptor.pluginId.idString})" + } + ), + OpenPluginDirectory(), + OpenPluginDescriptor(), + ), + ) + setEmptyState("No plugins") + TableSpeedSearch.installOn(this) + } + cell(ScrollPaneFactory.createScrollPane(pluginOverviewTable, false)) + .applyToComponent { preferredSize = Dimension(preferredSize.width, 350) } + .resizableColumn() + .align(Align.FILL) + } + .bottomGap(BottomGap.NONE) + row { button("Refresh") { populatePluginOverviewTableModel() } }.topGap(TopGap.NONE) + + group("Find Plugin by Class Name") { + row { + val classNameTextField = + textField().label("Class name:").resizableColumn().align(Align.FILL).component + button("Find") { findPluginByClassName(classNameTextField.text.trim()) } + } + } + } + + group("Plugin Class Loaders") { + row { + cell( + ScrollPaneFactory.createScrollPane( + Tree(traverseClassLoader(Thread.currentThread().contextClassLoader)), + false, + ) + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + + group("Find Class File Path by Class Name") { + row { + val classNameTextField = + textField().label("Class name:").resizableColumn().align(Align.FILL).component + button("Find") { findClassPathByClassName(classNameTextField.text.trim()) } + } + } + } + } + + override fun afterBuildUi() { + populatePluginOverviewTableModel() + } + + override fun getData(dataId: String): Any? = + when { + PluginCommonDataKeys.SELECTED_VALUES.`is`(dataId) -> { + val tableModel = + pluginOverviewTable.model.uncheckedCastTo>() + val rowSorter = pluginOverviewTable.rowSorter + pluginOverviewTable.selectedRows + .map { tableModel.getRowValue(rowSorter.convertRowIndexToModel(it)) } + .toList() + } + + else -> null + } + + // -- Private Methods ----------------------------------------------------- // + + private fun populatePluginOverviewTableModel() { + pluginOverviewTable.model.uncheckedCastTo>().items = + PluginManager.getPlugins().sortedBy { it.name } + } + + private fun findPluginByClassName(className: String) { + val messageDialogTitle = "Find Plugin by Class Name" + try { + val plugin = PluginManager.getPluginByClass(Class.forName(className)) + if (plugin != null) { + Messages.showInfoMessage( + project, + "Class belongs to plugin: ${plugin.name} (ID: ${plugin.pluginId.idString}).", + messageDialogTitle, + ) + } else { + Messages.showErrorDialog( + project, + "No plugin found for the given class name.", + messageDialogTitle, + ) + } + } catch (e: Exception) { + log.warn("Failed to find plugin", e) + Messages.showErrorDialog( + project, + "${e.message}: ${e::class.qualifiedName}", + messageDialogTitle, + ) + } + } + + private fun findClassPathByClassName(className: String) { + val messageDialogTitle = "Find Class File Path by Class Name" + try { + val aClass = IntelliJInternals::class.java.classLoader.loadClass(className) + val classFilePath = + aClass + .getResource('/' + aClass.getName().replace('.', '/') + ".class") + ?.toURI() + ?.path + ?.toString() + if (classFilePath != null) { + Messages.showInfoMessage(project, "Class file path: ${classFilePath}.", messageDialogTitle) + } else { + Messages.showErrorDialog( + project, + "Unable to resolve the path of the class file for the given class name.", + messageDialogTitle, + ) + } + } catch (e: Exception) { + log.warn("Failed to find class file", e) + Messages.showErrorDialog( + project, + "${e.message}: ${e::class.qualifiedName}", + messageDialogTitle, + ) + } + } + + private fun traverseClassLoader(classLoader: ClassLoader): DefaultMutableTreeNode { + val classLoaderNode = + DefaultMutableTreeNode("Class loader: ${classLoader::class.qualifiedName}") + + if (classLoader is URLClassLoader) { + classLoader.urLs.forEach { url -> + classLoaderNode.add(DefaultMutableTreeNode("File: ${Paths.get(url.toURI().path).fileName}")) + } + } else if (classLoader is UrlClassLoader) { + classLoader.files.forEach { file -> + classLoaderNode.add(DefaultMutableTreeNode("File: ${file.fileName}")) + } + } + + val parentClassLoader = classLoader.parent + if (parentClassLoader != null) { + val parentClassLoaderNode = traverseClassLoader(parentClassLoader) + classLoaderNode.add(parentClassLoaderNode) + } + + return classLoaderNode + } + + // -- Inner Type ---------------------------------------------------------- // + + private class OpenPluginDirectory : DumbAwareAction("Open Plugin Directory") { + + override fun actionPerformed(e: AnActionEvent) { + val values: List = + PluginCommonDataKeys.SELECTED_VALUES.getData(e.dataContext) + ?: throw IllegalStateException("snh: Data missing") + if (values.isEmpty()) { + return + } + + values.map { it as PluginDescriptor }.forEach { BrowserUtil.browse(it.pluginPath) } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class OpenPluginDescriptor : DumbAwareAction("Open Plugin Descriptor") { + + override fun actionPerformed(e: AnActionEvent) { + val project = + e.dataContext.getData(CommonDataKeys.PROJECT) + ?: throw IllegalStateException("snh: Data missing") + val values: List = + PluginCommonDataKeys.SELECTED_VALUES.getData(e.dataContext) + ?: throw IllegalStateException("snh: Data missing") + if (values.isEmpty()) { + return + } + + findPluginDescriptorFiles(values.map { it as IdeaPluginDescriptor }) { pluginDescriptorFiles + -> + openProgramDescriptorFile(pluginDescriptorFiles, project) + } + } + + private fun findPluginDescriptorFiles( + pluginDescriptors: List, + callback: (List) -> Unit, + ) { + ApplicationManager.getApplication().executeOnPooledThread { + val virtualFileManager = VirtualFileManager.getInstance() + val pluginDescriptorFiles = + pluginDescriptors.flatMap { pluginDescriptor -> + Files.list(pluginDescriptor.pluginPath.resolve("lib")).use { files -> + files + .asSequence() + .filter { Files.isRegularFile(it) && it.extension() == "jar" } + .mapNotNull { + virtualFileManager.findFileByUrl( + "jar://${it.toAbsolutePath()}!/${PluginManagerCore.PLUGIN_XML_PATH}" + ) + } + .toList() + } + } + callback(pluginDescriptorFiles) + } + } + + private fun openProgramDescriptorFile( + pluginDescriptorFiles: List, + project: Project, + ) { + invokeLater { + if (pluginDescriptorFiles.isEmpty()) { + Messages.showErrorDialog( + project, + "Unable to find plugin descriptor.", + "Open Plugin Descriptor", + ) + } + + val fileEditorManager = FileEditorManager.getInstance(project) + pluginDescriptorFiles.forEach { + val openEditor = fileEditorManager.openEditor(OpenFileDescriptor(project, it), true) + if (openEditor.isEmpty()) { + Messages.showErrorDialog( + project, + "Unable to open file '${it.name}' in editor.", + "Open Plugin Descriptor", + ) + } + } + } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = "IntelliJ Internals", + contentTitle = "IntelliJ Internals", + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> IntelliJInternals) = { _ -> + IntelliJInternals(parentDisposable, project) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val log = logger() + + private val pluginOverviewTableColumns: Array> = + arrayOf( + simpleColumnInfo("Name", { it.name }, { it.name }), + simpleColumnInfo("ID", { it.pluginId.idString }, { it.pluginId.idString }), + ) + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/JsonSchemaValidator.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/JsonSchemaValidator.kt new file mode 100644 index 00000000..be2ce2f7 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/JsonSchemaValidator.kt @@ -0,0 +1,273 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.fasterxml.jackson.databind.JsonNode +import com.intellij.icons.AllIcons +import com.intellij.json.JsonLanguage +import com.intellij.openapi.Disposable +import com.intellij.openapi.observable.properties.AtomicProperty +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.selected +import com.intellij.ui.layout.not +import com.networknt.schema.JsonSchema +import com.networknt.schema.JsonSchemaFactory +import com.networknt.schema.SpecVersion +import com.networknt.schema.SpecVersionDetector +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.ObjectMapperService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.PropertyComponentPredicate + +class JsonSchemaValidator( + private val context: DeveloperUiToolContext, + private val configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + private val project: Project?, +) : DeveloperUiTool(parentDisposable) { + // -- Properties ---------------------------------------------------------- // + + private var liveValidation = configuration.register("liveValidation", true) + private val schemaText = + configuration.register("schemaText", "", PropertyType.INPUT, EXAMPLE_SCHEMA) + private val dataText = configuration.register("dataText", "", PropertyType.INPUT, EXAMPLE_DATA) + + private val schemaEditor by lazy { this.createSchemaEditor() } + private val schemaErrorHolder = ErrorHolder() + private val dataEditor by lazy { this.createDataEditor() } + private val dataErrorHolder = ErrorHolder() + + private val validationState: ObservableMutableProperty = + AtomicProperty(ValidationState.VALIDATED) + private val validationError: ObservableMutableProperty = AtomicProperty("") + + // -- Initialization ------------------------------------------------------ // + + init { + liveValidation.afterChange { + if (it) { + validateSchema() + } + } + } + + // -- Exposed Methods ----------------------------------------------------- // + + override fun Panel.buildUi() { + row { + cell(schemaEditor.component) + .align(Align.FILL) + .validationOnApply(schemaEditor.bindValidator(schemaErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + } + .resizableRow() + + row { + val liveValidationCheckBox = + checkBox("Live validation").bindSelected(liveValidation).gap(RightGap.SMALL) + + button("Validate") { validateSchema() } + .enabledIf(liveValidationCheckBox.selected.not()) + .gap(RightGap.SMALL) + } + + row { + cell(dataEditor.component) + .align(Align.FILL) + .validationOnApply(dataEditor.bindValidator(dataErrorHolder.asValidation())) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + } + .resizableRow() + + row { + icon(AllIcons.General.InspectionsOK).gap(RightGap.SMALL) + label("Data matches schema") + } + .visibleIf(PropertyComponentPredicate(validationState, ValidationState.VALIDATED)) + + row { + icon(AllIcons.General.BalloonError).gap(RightGap.SMALL) + label("").bindText(validationError) + } + .visibleIf(PropertyComponentPredicate(validationState, ValidationState.ERROR)) + + row { + icon(AllIcons.General.Warning).gap(RightGap.SMALL) + label("Invalid input") + } + .visibleIf(PropertyComponentPredicate(validationState, ValidationState.INVALID_INPUT)) + } + + override fun afterBuildUi() { + if (liveValidation.get()) { + validateSchema() + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun validateSchema() { + schemaErrorHolder.clear() + dataErrorHolder.clear() + + val jsonMapper = ObjectMapperService.instance.jsonMapper() + + val schema: JsonSchema? = + try { + val schemaNode = jsonMapper.readTree(schemaEditor.text) + val versionFlag = SpecVersionDetector.detect(schemaNode) + JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7).getSchema(schemaEditor.text) + } catch (e: Exception) { + schemaErrorHolder.add(e) + validationState.set(ValidationState.INVALID_INPUT) + null + } + + val dataNode: JsonNode? = + try { + jsonMapper.readTree(dataEditor.text) + } catch (e: Exception) { + dataErrorHolder.add(e) + validationState.set(ValidationState.INVALID_INPUT) + null + } + + // The `validate` in this class is not used as a validation mechanism. We + // make use of its text field error UI to display the `errorHolder`. + validate() + + if (schema != null && dataNode != null) { + val errors = schema.validate(dataNode) + if (errors.isEmpty()) { + validationState.set(ValidationState.VALIDATED) + } else { + validationState.set(ValidationState.ERROR) + validationError.set( + """ + + Data does not match schema:
    + ${errors.joinToString(separator = "
    ") { "- $it" }} + + """ + .trimIndent() + ) + } + } + } + + private fun createSchemaEditor() = + AdvancedEditor( + id = "schema", + context = context, + configuration = configuration, + project = project, + title = "JSON schema", + editorMode = INPUT, + parentDisposable = parentDisposable, + initialLanguage = JsonLanguage.INSTANCE, + textProperty = schemaText, + ) + .apply { + onTextChangeFromUi { + if (liveValidation.get()) { + validateSchema() + } + } + } + + private fun createDataEditor() = + AdvancedEditor( + id = "data", + context = context, + configuration = configuration, + project = project, + title = "JSON data", + editorMode = INPUT, + parentDisposable = parentDisposable, + initialLanguage = JsonLanguage.INSTANCE, + textProperty = dataText, + ) + .apply { + onTextChangeFromUi { + if (liveValidation.get()) { + validateSchema() + } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class ValidationState { + + VALIDATED, + ERROR, + INVALID_INPUT, + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "JSON Schema", contentTitle = "JSON Schema Validator") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> JsonSchemaValidator) = { configuration -> + JsonSchemaValidator(context, configuration, parentDisposable, project) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val EXAMPLE_SCHEMA = + """ +{ + "${'$'}id": "https://example.com/person.schema.json", + "${'$'}schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + } + } +} + """ + .trimIndent() + private val EXAMPLE_DATA = + """ +{ + "firstName": "John", + "lastName": "Doe", + "age": 21 +} + """ + .trimIndent() + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/Notes.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/Notes.kt new file mode 100644 index 00000000..b9305a0f --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/Notes.kt @@ -0,0 +1,66 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.Panel +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor + +class Notes( + private val context: DeveloperUiToolContext, + private val configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + private val project: Project?, +) : DeveloperUiTool(parentDisposable = parentDisposable) { + // -- Properties ---------------------------------------------------------- // + + private val text = configuration.register("test", "", INPUT) + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun Panel.buildUi() { + row { + cell( + AdvancedEditor( + id = "content", + context = context, + configuration = configuration, + project = project, + editorMode = AdvancedEditor.EditorMode.INPUT, + parentDisposable = parentDisposable, + textProperty = text, + ) + .component + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Notes", contentTitle = "Notes") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> Notes) = { configuration -> + Notes(context, configuration, parentDisposable, project) + } + } + + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RegularExpressionMatcher.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RegularExpressionMatcher.kt new file mode 100644 index 00000000..3083e65d --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RegularExpressionMatcher.kt @@ -0,0 +1,560 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.google.code.regexp.Pattern +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.editor.colors.EditorColors.SEARCH_RESULT_ATTRIBUTES +import com.intellij.openapi.editor.colors.EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.markup.HighlighterLayer +import com.intellij.openapi.observable.util.whenFocusLost +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Splitter +import com.intellij.openapi.ui.setEmptyState +import com.intellij.openapi.util.TextRange +import com.intellij.ui.ColoredTableCellRenderer +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.SimpleTextAttributes.GRAY_SMALL_ATTRIBUTES +import com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES +import com.intellij.ui.SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES +import com.intellij.ui.TableSpeedSearch +import com.intellij.ui.components.JBTabbedPane +import com.intellij.ui.components.JBViewport +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.LabelPosition +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.builder.whenTextChangedFromUi +import com.intellij.ui.table.JBTable +import com.intellij.util.ui.JBUI +import dev.turingcomplete.intellijdevelopertoolsplugin.common.CopyValuesAction +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginCommonDataKeys +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.getOrNull +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex.RegexTextField +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex.SelectRegexOptionsAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.setContextMenu +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other.RegularExpressionMatcher.MatchResultType.MATCH +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other.RegularExpressionMatcher.MatchResultType.NAMED_GROUP +import java.awt.Dimension +import javax.swing.BorderFactory +import javax.swing.JTable +import javax.swing.ListSelectionModel +import javax.swing.event.ListSelectionListener +import javax.swing.table.AbstractTableModel + +class RegularExpressionMatcher( + private val context: DeveloperUiToolContext, + private val configuration: DeveloperToolConfiguration, + private val project: Project?, + parentDisposable: Disposable, +) : DeveloperUiTool(parentDisposable) { + // -- Properties ---------------------------------------------------------- // + + private var selectedRegexOptionFlag = configuration.register("regexOption", 0) + + private val regexPattern = configuration.register("regexText", "", INPUT, EXAMPLE_REGEX) + private val inputText = configuration.register("inputText", "", INPUT, EXAMPLE_INPUT_TEXT) + private val substitutionPattern = + configuration.register("substitutionPattern", "", INPUT, EXAMPLE_SUBSTITUTION_PATTERN) + private val extractionPattern = + configuration.register("extractionPattern", "", INPUT, EXAMPLE_EXTRACTION_PATTERN) + + private val substitutionResult = ValueProperty("") + private val extractionResult = ValueProperty("") + private lateinit var inputEditor: AdvancedEditor + + private val regexMatchingAttributes by lazy { + EditorColorsManager.getInstance().globalScheme.getAttributes(SEARCH_RESULT_ATTRIBUTES) + } + private val selectedMatchResultHighlightingAttributes by lazy { + EditorColorsManager.getInstance().globalScheme.getAttributes(TEXT_SEARCH_RESULT_ATTRIBUTES) + } + private lateinit var matchResultsTableModel: MatchResultsTableModel + + private val regexInputErrorHolder = ErrorHolder() + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun Panel.buildUi() { + row { + cell( + Splitter(true, 0.7f).apply { + firstComponent = createInputComponent() + secondComponent = createResultComponent() + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + } + + override fun afterBuildUi() { + sync() + } + + override fun reset() { + sync() + } + + // -- Private Methods ----------------------------------------------------- // + + private fun createInputComponent() = panel { + row { + val regexTextField = + RegexTextField(project, parentDisposable, regexPattern).onTextChangeFromUi { sync() } + cell(regexTextField) + .label(UiToolsBundle.message("regular-expression-matcher.regex-input"), LabelPosition.TOP) + .validationOnApply(regexInputErrorHolder.asValidation()) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .align(Align.FILL) + .resizableColumn() + .gap(RightGap.SMALL) + cell(SelectRegexOptionsAction.createActionButton(selectedRegexOptionFlag)) + } + .topGap(TopGap.NONE) + + row { + inputEditor = + AdvancedEditor( + id = "input", + context = context, + configuration = configuration, + project = project, + title = UiToolsBundle.message("regular-expression-matcher.text-input-title"), + editorMode = AdvancedEditor.EditorMode.INPUT, + parentDisposable = parentDisposable, + textProperty = inputText, + ) + .onTextChangeFromUi { sync() } + cell(inputEditor.component).align(Align.FILL) + } + .resizableRow() + .topGap(TopGap.SMALL) + } + + private fun createResultComponent() = panel { + row { + cell( + JBTabbedPane().apply { + addTab( + UiToolsBundle.message("regular-expression-matcher.matches-title"), + createMatchesTableComponent(), + ) + + addTab( + UiToolsBundle.message("regular-expression-matcher.substitution-title"), + createSubstitutionComponent(), + ) + + addTab( + UiToolsBundle.message("regular-expression-matcher.extraction-title"), + createExtractionComponent(), + ) + } + ) + .resizableColumn() + .align(Align.FILL) + } + .resizableRow() + } + + private fun createMatchesTableComponent() = panel { + row { + val highlightSelectedMatchResults: (List) -> Unit = { textRanges -> + inputEditor.removeTextRangeHighlighters(SELECTED_MATCH_RESULT_HIGHLIGHTING_GROUP_ID) + textRanges.forEach { textRange -> + inputEditor.highlightTextRange( + textRange, + REGEX_MATCH_SELECTED_HIGHLIGHT_LAYER, + selectedMatchResultHighlightingAttributes, + SELECTED_MATCH_RESULT_HIGHLIGHTING_GROUP_ID, + ) + } + } + matchResultsTableModel = MatchResultsTableModel() + val matchResultsTable = + MatchResultsTable(matchResultsTableModel, highlightSelectedMatchResults).apply { + whenFocusLost(parentDisposable) { + inputEditor.removeTextRangeHighlighters(SELECTED_MATCH_RESULT_HIGHLIGHTING_GROUP_ID) + } + } + cell( + ScrollPaneFactory.createScrollPane(matchResultsTable).apply { + minimumSize = Dimension(minimumSize.width, 150) + preferredSize = Dimension(preferredSize.width, 150) + } + ) + .align(Align.FILL) + } + .resizableRow() + } + + @Suppress("UnstableApiUsage") + private fun createSubstitutionComponent() = panel { + row { + expandableTextField() + .bindText(substitutionPattern) + .align(Align.FILL) + .resizableColumn() + .whenTextChangedFromUi { substitute() } + .gap(RightGap.SMALL) + contextHelp(UiToolsBundle.message("regular-expression-matcher.replace-pattern-context-help")) + } + + row { + cell( + AdvancedEditor( + id = "substitution-result", + context = context, + configuration = configuration, + project = project, + editorMode = AdvancedEditor.EditorMode.OUTPUT, + parentDisposable = parentDisposable, + textProperty = substitutionResult, + ) + .component + ) + .align(Align.FILL) + } + .resizableRow() + .topGap(TopGap.SMALL) + } + + @Suppress("UnstableApiUsage") + private fun createExtractionComponent() = panel { + row { + expandableTextField() + .bindText(extractionPattern) + .align(Align.FILL) + .resizableColumn() + .whenTextChangedFromUi { extract() } + .gap(RightGap.SMALL) + contextHelp(UiToolsBundle.message("regular-expression-matcher.replace-pattern-context-help")) + } + + row { + cell( + AdvancedEditor( + id = "extraction-result", + context = context, + configuration = configuration, + project = project, + editorMode = AdvancedEditor.EditorMode.OUTPUT, + parentDisposable = parentDisposable, + textProperty = extractionResult, + ) + .component + ) + .align(Align.FILL) + } + .resizableRow() + .topGap(TopGap.SMALL) + } + + private fun sync() { + match() + substitute() + extract() + } + + /** + * Kotlin and Java do not support the retrieval of all named groups yet: + * [KT-51671](https://youtrack.jetbrains.com/issue/KT-51671). Therefore, we are using Google's + * [com.google.code.regexp.Pattern] for now. + */ + private fun match() { + inputEditor.removeAllTextRangeHighlighters() + regexInputErrorHolder.clear() + matchResultsTableModel.setMatches(emptyList()) + + val regex = regexPattern.get() + if (regex.isEmpty()) { + return + } + + try { + val pattern = Pattern.compile(regex, selectedRegexOptionFlag.get()) + val matcher = pattern.matcher(inputEditor.text) + + val namedGroups = matcher.namedGroupsList() + val results = mutableListOf() + var i = 0 + while (matcher.find()) { + val textRange = TextRange(matcher.start(), matcher.end()) + inputEditor.highlightTextRange( + textRange, + REGEX_MATCH_HIGHLIGHT_LAYER, + regexMatchingAttributes, + ) + results.add(Match(i, "${i + 1}", textRange, matcher.group(), MATCH)) + if (namedGroups.isNotEmpty()) { + namedGroups[i] + .filter { it.value != null } + .forEach { + results.add( + Match( + i, + it.key, + TextRange(matcher.start(it.key), matcher.end(it.key)), + it.value, + NAMED_GROUP, + ) + ) + } + } + i++ + } + + matchResultsTableModel.setMatches(results) + } catch (e: Exception) { + regexInputErrorHolder.add(e) + } + + // The `validate` in this class is not used as a validation mechanism. We + // make use of its text field error UI to display the `regexInputErrorHolder`. + validate() + } + + private fun substitute() { + val regex = regexPattern.get() + if (regex.isEmpty()) { + return + } + + try { + val result = + Regex(regex).replace(inputText.get()) { matchResult -> + substitutionPattern.get().replace(Regex("""\$(\d+)|\$\{(\d+)}""")) { groupMatch -> + val groupIndex = + groupMatch.groups[1]?.value?.toInt() ?: groupMatch.groups[2]?.value?.toInt() ?: -1 + matchResult.groups.getOrNull(groupIndex)?.value ?: "$$groupIndex" + } + } + substitutionResult.set(result) + } catch (_: Exception) { + // An invalid pattern will be handled by `match`. + } + } + + private fun extract() { + val regex = regexPattern.get() + if (regex.isEmpty()) { + return + } + + try { + val result = + Regex(regex) + .findAll(inputText.get()) + .map { matchResult -> + extractionPattern.get().replace(Regex("""\$(\d+)|\$\{(\d+)}""")) { groupMatch -> + val groupIndex = + groupMatch.groups[1]?.value?.toInt() ?: groupMatch.groups[2]?.value?.toInt() ?: -1 + matchResult.groups.getOrNull(groupIndex)?.value ?: "$$groupIndex" + } + } + .joinToString(separator = "") + extractionResult.set(result) + } catch (_: Exception) { + // An invalid pattern will be handled by `match`. + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class MatchResultsTable( + private val model: MatchResultsTableModel, + private val selectedMatchResultHighlight: (List) -> Unit, + ) : JBTable(model), DataProvider { + + init { + columnModel.apply { + getColumn(0).preferredWidth = 150 + getColumn(1).preferredWidth = 300 + } + visibleRowCount = 4 + putClientProperty(JBViewport.FORCE_VISIBLE_ROW_COUNT_KEY, true) + autoResizeMode = AUTO_RESIZE_LAST_COLUMN + setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) + rowSelectionAllowed = true + columnSelectionAllowed = false + setDefaultRenderer(Object::class.java, MatchResultsTableCellRenderer(model)) + selectionModel.addListSelectionListener(createSelectionListener()) + setContextMenu(this::class.java.name, DefaultActionGroup(CopyValuesAction())) + setEmptyState(UiToolsBundle.message("regular-expression-matcher.matches-no-matches")) + TableSpeedSearch.installOn(this) { value, cell -> + if (cell.column == 0 || cell.column == 1) value as String else null + } + } + + override fun getData(dataId: String): Any? = + when { + PluginCommonDataKeys.SELECTED_VALUES.`is`(dataId) -> + selectedRows.map { model.getValueAt(it, 1) as String }.toList() + else -> null + } + + @Suppress("UNCHECKED_CAST") + private fun createSelectionListener() = ListSelectionListener { e -> + if (!e.valueIsAdjusting) { + val selectedTextRanges = + this@MatchResultsTable.selectedRows + .map { (model.getValueAt(it, 0) as Pair).second } + .toList() + selectedMatchResultHighlight(selectedTextRanges) + } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private data class Match( + val groupIndex: Int, + val title: String, + val textRange: TextRange, + val value: String, + val matchResultType: MatchResultType, + ) + + // -- Inner Type ---------------------------------------------------------- // + + private class MatchResultsTableModel : AbstractTableModel() { + + private var matches = listOf() + + fun setMatches(matches: List) { + this.matches = matches + fireTableDataChanged() + } + + override fun getRowCount(): Int = matches.size + + override fun getColumnCount(): Int = 2 + + override fun getColumnName(column: Int): String = + when (column) { + 0 -> UiToolsBundle.message("regular-expression-matcher.matches-group") + 1 -> UiToolsBundle.message("regular-expression-matcher.matches-value") + else -> error("Unknown column: $column") + } + + override fun getValueAt(row: Int, column: Int): Any = + when (column) { + 0, + 1 -> matches[row] + else -> error("Unknown column: $column") + } + + fun getRowMatchResultType(row: Int): MatchResultType = matches[row].matchResultType + } + + // -- Inner Type ---------------------------------------------------------- // + + private class MatchResultsTableCellRenderer(private val model: MatchResultsTableModel) : + ColoredTableCellRenderer() { + + @Suppress("UNCHECKED_CAST") + override fun customizeCellRenderer( + table: JTable, + match: Any?, + selected: Boolean, + hasFocus: Boolean, + row: Int, + column: Int, + ) { + check(match is Match) + + val matchResultType = model.getRowMatchResultType(row) + + when (column) { + 0 -> { + val prefix = + when (matchResultType) { + MATCH -> UiToolsBundle.message("regular-expression-matcher.matches-match-prefix") + NAMED_GROUP -> + UiToolsBundle.message("regular-expression-matcher.matches-group-prefix") + } + append("$prefix ", REGULAR_ATTRIBUTES) + append("${match.title} ", REGULAR_BOLD_ATTRIBUTES) + append( + "(${match.textRange.startOffset} to ${match.textRange.endOffset})", + GRAY_SMALL_ATTRIBUTES, + ) + } + 1 -> append(match.value) + else -> error("Unknown column: $column") + } + + border = + if (row > 0 && matchResultType == MATCH) { + BorderFactory.createCompoundBorder(matchResultAfterFirstMatchBorder, border) + } else if (column == 0 && matchResultType == NAMED_GROUP) { + BorderFactory.createCompoundBorder(matchResultGroupBorder, border) + } else { + border + } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class MatchResultType { + + MATCH, + NAMED_GROUP, + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("regular-expression-matcher.menu-title"), + contentTitle = UiToolsBundle.message("regular-expression-matcher.content-title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> RegularExpressionMatcher) = { configuration -> + RegularExpressionMatcher(context, configuration, project, parentDisposable) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val EXAMPLE_REGEX = "mid(?[a-zA-Z]+)" + private const val EXAMPLE_INPUT_TEXT = "aurora midsummer midnight earth" + private const val EXAMPLE_SUBSTITUTION_PATTERN = "deep \$1" + private const val EXAMPLE_EXTRACTION_PATTERN = "\${1}s " + + private const val REGEX_MATCH_HIGHLIGHT_LAYER = HighlighterLayer.SELECTION - 2 + private const val REGEX_MATCH_SELECTED_HIGHLIGHT_LAYER = HighlighterLayer.SELECTION - 1 + + private const val SELECTED_MATCH_RESULT_HIGHLIGHTING_GROUP_ID = "matchResultHighlighting" + + private val matchResultAfterFirstMatchBorder = + JBUI.Borders.customLineTop(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()) + private val matchResultGroupBorder = JBUI.Borders.emptyLeft(5) + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/RubberDuck.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RubberDuck.kt similarity index 51% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/RubberDuck.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RubberDuck.kt index 245b86a8..87bfc4cb 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/RubberDuck.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/RubberDuck.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other import com.intellij.ide.BrowserUtil import com.intellij.openapi.Disposable @@ -7,20 +7,20 @@ import com.intellij.ui.components.JBLabel import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.Panel import com.intellij.util.ui.components.BorderLayoutPanel -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation import java.awt.Image.SCALE_SMOOTH import javax.imageio.ImageIO import javax.swing.ImageIcon import javax.swing.JLabel class RubberDuck(parentDisposable: Disposable) : DeveloperUiTool(parentDisposable) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun Panel.buildUi() { row { @@ -31,44 +31,55 @@ class RubberDuck(parentDisposable: Disposable) : DeveloperUiTool(parentDisposabl line to a rubber duck or any other inanimate object. The act of explaining the code helps the programmer to identify errors and logic mistakes in their code. This technique is widely used in software development to improve code quality and debugging efficiency. - """.trimMargin() + """ + .trimMargin() ) ) } row { - cell(BorderLayoutPanel().apply { - RubberDuck::class.java.getResourceAsStream("/dev/turingcomplete/intellijdevelopertoolsplugin/rubber-duck-yellow.png")?.use { - val read = ImageIO.read(it) - val scaledInstance = read.getScaledInstance(read.width.div(2), read.height.div(2), SCALE_SMOOTH) - addToCenter(JLabel(ImageIcon(scaledInstance))) - } - }).align(Align.CENTER) - }.resizableRow() + cell( + BorderLayoutPanel().apply { + RubberDuck::class + .java + .getResourceAsStream( + "/dev/turingcomplete/intellijdevelopertoolsplugin/rubber-duck-yellow.png" + ) + ?.use { + val read = ImageIO.read(it) + val scaledInstance = + read.getScaledInstance(read.width.div(2), read.height.div(2), SCALE_SMOOTH) + addToCenter(JLabel(ImageIcon(scaledInstance))) + } + } + ) + .align(Align.CENTER) + } + .resizableRow() row { - comment("Image by Anthony") { + comment( + "Image by Anthony" + ) { BrowserUtil.browse(it.url) } } } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Rubber Duck", - contentTitle = "Rubber Duck Debugging" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Rubber Duck", contentTitle = "Rubber Duck Debugging") override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> RubberDuck) = { RubberDuck(parentDisposable) } } - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ServerCertificates.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ServerCertificates.kt new file mode 100644 index 00000000..0d0344da --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/ServerCertificates.kt @@ -0,0 +1,698 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import ai.grazie.utils.capitalize +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.fileChooser.FileChooserFactory +import com.intellij.openapi.fileChooser.FileSaverDescriptor +import com.intellij.openapi.fileChooser.FileSaverDialog +import com.intellij.openapi.ide.CopyPasteManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.ui.HyperlinkAdapter +import com.intellij.ui.HyperlinkLabel +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBLabel +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.text.DateFormatUtil +import com.intellij.util.ui.components.BorderLayoutPanel +import dev.turingcomplete.intellijdevelopertoolsplugin.common.OkHttpClientUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.common.OkHttpClientUtils.applyIntelliJProxySettings +import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode.OUTPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AnActionOptionButton +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.createPopup +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.copyable +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.message.UiToolsBundle +import java.awt.datatransfer.StringSelection +import java.io.ByteArrayOutputStream +import java.math.BigInteger +import java.net.URI +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.StandardOpenOption +import java.security.KeyStore +import java.security.SecureRandom +import java.security.cert.Certificate +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException +import java.security.cert.X509Certificate +import java.util.Base64 +import java.util.Date +import java.util.StringJoiner +import javax.net.ssl.HostnameVerifier +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager +import javax.security.auth.x500.X500Principal +import javax.swing.JComponent +import javax.swing.event.HyperlinkEvent +import okhttp3.OkHttpClient +import okhttp3.Request + +class ServerCertificates( + private val project: Project?, + private val context: DeveloperUiToolContext, + private val configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, +) : DeveloperUiTool(parentDisposable), DataProvider { + // -- Properties ---------------------------------------------------------- // + + private val log = logger() + + private val url = configuration.register("url", "", INPUT, "https://jetbrains.com") + private val followRedirects = configuration.register("followRedirects", true, CONFIGURATION) + private val allowInsecureConnection = + configuration.register("allowInsecureConnection", false, CONFIGURATION) + private val certificatesPanel = BorderLayoutPanel() + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun Panel.buildUi() { + row { + expandableTextField() + .label(UiToolsBundle.message("server-certificates.url")) + .bindText(url) + .resizableColumn() + .align(Align.FILL) + } + .bottomGap(BottomGap.NONE) + row { + checkBox(UiToolsBundle.message("server-certificates.follow-redirects")) + .bindSelected(followRedirects) + } + .topGap(TopGap.NONE) + row { + checkBox(UiToolsBundle.message("server-certificates.allow-insecure-connection")) + .bindSelected(allowInsecureConnection) + .gap(RightGap.SMALL) + contextHelp(UiToolsBundle.message("server-certificates.allow-insecure-connection-help")) + } + .topGap(TopGap.NONE) + + row { + button(UiToolsBundle.message("server-certificates.fetch-server-certificates")) { + val url = url.get() + fetchCertificates( + project = project, + url = url, + allowInsecureConnection = allowInsecureConnection.get(), + onStarted = { setCertificatesResultUi(createFetchingUi()) }, + onSuccess = { setCertificatesResultUi(createCertificatesUi(it)) }, + onCancel = { setCertificatesResultUi(null) }, + onThrowable = { e -> + log.warn("Failed to retrieve server certificates from: $url", e) + setCertificatesResultUi(createFetchingFailedUi(e)) + }, + ) + } + } + + row { cell(certificatesPanel).resizableColumn().align(Align.FILL) } + .resizableRow() + .topGap(TopGap.MEDIUM) + } + + private fun setCertificatesResultUi(component: JComponent?) { + ApplicationManager.getApplication().invokeLater { + certificatesPanel.removeAll() + if (component != null) { + certificatesPanel.addToCenter(component) + } + certificatesPanel.revalidate() + certificatesPanel.repaint() + } + } + + // -- Private Methods ----------------------------------------------------- // + + private fun createFetchingUi(): JComponent = panel { + row { + label(UiToolsBundle.message("server-certificates.fetch-server-certificates-in-progress")) + .align(Align.FILL) + .resizableColumn() + } + } + + private fun createFetchingFailedUi(e: Throwable): JComponent = panel { + row { + icon(AllIcons.General.BalloonError).gap(RightGap.SMALL) + label( + UiToolsBundle.message( + "server-certificates.fetch-server-certificates-failed", + "${e::class.simpleName}: ${e.message}", + ) + ) + .align(Align.FILL) + .resizableColumn() + } + } + + private fun createCertificatesUi(httpResponse: HttpResponse): JComponent = panel { + group(UiToolsBundle.message("server-certificates.result"), false) { + row { + cell( + HyperlinkLabel( + UiToolsBundle.message( + "server-certificates.response", + httpResponse.statusCode, + httpResponse.statusMessage, + ) + ) + .apply { + addHyperlinkListener(createShowHttpResponseHyperlinkHandler(httpResponse, this)) + } + ) + } + + if (httpResponse.certificates?.isNotEmpty() == true) { + buildCertificatesExportUi(httpResponse.certificates) + + httpResponse.certificates.forEachIndexed { index, certificate -> + group(UiToolsBundle.message("server-certificates.certificate-title", index + 1), false) { + if (certificate is X509Certificate) { + buildCertificatePropertiesUi(certificate) + buildCertificateValidityUi(certificate) + } + buildCertificatesExportUi(listOf(certificate)) + } + } + } else { + row { label("${UiToolsBundle.message("server-certificates.no-result")}") } + } + } + } + + private fun createShowHttpResponseHyperlinkHandler( + httpResponse: HttpResponse, + parentComponent: JComponent, + ): HyperlinkAdapter = + object : HyperlinkAdapter() { + override fun hyperlinkActivated(e: HyperlinkEvent) { + val content = panel { + row { + cell( + AdvancedEditor( + id = "server-certificates-http-response", + context = context, + configuration = configuration, + project = project, + title = null, + editorMode = OUTPUT, + parentDisposable = parentDisposable, + ) + .apply { + text = + with(StringJoiner(System.lineSeparator())) { + add( + "${httpResponse.protocol} ${httpResponse.statusCode} ${httpResponse.statusMessage}" + ) + httpResponse.headers.forEach { + add("${it.key}: ${it.value.joinToString(", ") { it ?: "" }}") + } + httpResponse.body?.let { + add("") + add(it) + } + toString() + } + } + .component + ) + .resizableColumn() + .align(Align.FILL) + } + .resizableRow() + } + createPopup(content).showInCenterOf(parentComponent) + } + } + + private fun Panel.buildCertificatesExportUi(certificates: List) { + row { + lateinit var exportActionsButton: JComponent + exportActionsButton = + AnActionOptionButton( + ShowAsPemAction(certificates, context, configuration, project, parentDisposable) { + exportActionsButton + }, + ExportAsPemAction(certificates, url.get()), + ExportAsDerAction(certificates, url.get()), + ExportAsJksAction(certificates, url.get()), + CopyAsPemToClipboardAction(certificates), + ShowCertificateDetailsAction( + certificates, + context, + configuration, + project, + parentDisposable, + ) { + exportActionsButton + }, + ) + cell(exportActionsButton) + } + } + + private fun Panel.buildCertificatePropertiesUi(certificate: X509Certificate) { + listOf>( + UiToolsBundle.message("server-certificates.certificate-subject") to + certificate.subjectX500Principal, + UiToolsBundle.message("server-certificates.certificate-issuer") to + certificate.issuerX500Principal, + UiToolsBundle.message("server-certificates.certificate-serial-number") to + certificate.serialNumber, + UiToolsBundle.message("server-certificates.certificate-valid-from") to + certificate.notBefore, + UiToolsBundle.message("server-certificates.certificate-valid-to") to certificate.notAfter, + UiToolsBundle.message("server-certificates.certificate-signature-algorithm") to + certificate.sigAlgName, + ) + .forEach { (title, value) -> + row("$title:") { + val stringValue = + when (value) { + is String -> value + is BigInteger -> value.toString(16).uppercase() + is X500Principal -> value.toString() + is Date -> { + val diff = DateFormatUtil.formatBetweenDates(value.time, System.currentTimeMillis()) + "${DateFormatUtil.formatDateTime(value)} (${diff.capitalize()})" + } + + else -> throw IllegalStateException("Unknown property type: ${value::class}") + } + cell(JBLabel(stringValue).copyable()).gap(RightGap.SMALL) + } + } + } + + private fun Panel.buildCertificateValidityUi(certificate: X509Certificate) { + row { + try { + certificate.checkValidity() + } catch (_: CertificateExpiredException) { + icon(AllIcons.General.Warning).gap(RightGap.SMALL) + label(UiToolsBundle.message("server-certificates.certificate-expired")) + } catch (_: CertificateNotYetValidException) { + icon(AllIcons.General.Warning).gap(RightGap.SMALL) + label(UiToolsBundle.message("server-certificates.certificate-not-valid-yet")) + } + } + } + + private fun fetchCertificates( + project: Project?, + url: String, + allowInsecureConnection: Boolean, + onStarted: () -> Unit, + onSuccess: (HttpResponse) -> Unit, + onCancel: () -> Unit, + onThrowable: (Throwable) -> Unit, + ) { + object : + Task.Backgroundable( + project, + UiToolsBundle.message("server-certificates.fetch-server-certificates-in-progress-title"), + true, + ) { + override fun run(indicator: ProgressIndicator) { + indicator.text = + UiToolsBundle.message("server-certificates.fetch-server-certificates-in-progress") + onStarted() + + val httpClientBuilder = + OkHttpClient.Builder() + .followRedirects(followRedirects.get()) + .followSslRedirects(followRedirects.get()) + .applyIntelliJProxySettings(url) + + val certificateCapturingTrustManager = + CertificateCapturingTrustManager(allowInsecureConnection) + httpClientBuilder.sslSocketFactory( + certificateCapturingTrustManager.createSslContext().socketFactory, + certificateCapturingTrustManager, + ) + if (allowInsecureConnection) { + httpClientBuilder.hostnameVerifier(HostnameVerifier { _, _ -> true }) + } + + val httpClient = httpClientBuilder.build() + val request = Request.Builder().url(url).build() + val response = httpClient.newCall(request).execute() + val httpResponse = + HttpResponse( + certificates = certificateCapturingTrustManager.serverCertificates, + protocol = OkHttpClientUtils.toDisplayableString(response.protocol), + statusCode = response.code, + statusMessage = OkHttpClientUtils.toStatusMessage(response.code) ?: "", + headers = response.headers.toMultimap(), + body = response.body.string(), + ) + onSuccess(httpResponse) + } + + override fun onCancel() { + onCancel() + } + + override fun onThrowable(error: Throwable) { + onThrowable(error) + } + } + .queue() + } + + // -- Inner Type ---------------------------------------------------------- // + + private abstract class ExportCertificateAction( + private val formatName: String, + private val certificates: List, + private val url: String, + ) : + AnAction( + UiToolsBundle.message("server-certificates.export-action-title", formatName), + null, + AllIcons.Actions.MenuSaveall, + ) { + + override fun actionPerformed(e: AnActionEvent) { + try { + val fileSaverDescriptor = FileSaverDescriptor(e.presentation.text, "") + val saveFileDialog: FileSaverDialog = + FileChooserFactory.getInstance().createSaveFileDialog(fileSaverDescriptor, e.project) + val defaultFileName = createDefaultCertificateFileName() + saveFileDialog.save(defaultFileName)?.file?.toPath()?.let { targetPath -> + Files.write( + targetPath, + createFileContent(), + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.CREATE, + ) + } + onSuccess(e) + } catch (exception: Exception) { + val errorMessage = exception.message ?: "" + Messages.showErrorDialog( + e.project, + UiToolsBundle.message("server-certificates.export-failed", errorMessage), + e.presentation.text, + ) + } + } + + open fun onSuccess(e: AnActionEvent) { + // Override if needed + } + + abstract fun createFileContent(): ByteArray + + fun X509Certificate.getCn(): String? = + subjectX500Principal?.name?.let { + Regex("CN=(?[^,]+)").find(it)?.groups?.get("cn")?.value + } + + private fun createDefaultCertificateFileName(): String { + val fileName = + if (certificates.size > 1) { + try { + "server_certificates_chain_${URI.create(url).host.makeSafeForFilename()}" + } catch (_: Exception) { + "server_certificates_chain" + } + } else { + certificates[0].safeCastTo()?.getCn()?.makeSafeForFilename() + ?: "server_certificate" + } + return "$fileName.${formatName.lowercase()}" + } + + private fun String.makeSafeForFilename(): String = this.replace(Regex("[^a-zA-Z0-9]+"), "_") + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ExportAsPemAction(private val certificates: List, url: String) : + ExportCertificateAction("PEM", certificates, url) { + + override fun createFileContent(): ByteArray = + certificates + .flatMap { cert -> + listOf( + "-----BEGIN CERTIFICATE-----", + Base64.getMimeEncoder(64, "\n".toByteArray()).encodeToString(cert.encoded), + "-----END CERTIFICATE-----", + ) + } + .joinToString(System.lineSeparator()) + .toByteArray(StandardCharsets.UTF_8) + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ExportAsDerAction(private val certificates: List, url: String) : + ExportCertificateAction("DER", certificates, url) { + + override fun createFileContent(): ByteArray = + certificates.flatMap { it.encoded.asList() }.toByteArray() + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ExportAsJksAction(private val certificates: List, url: String) : + ExportCertificateAction("JKS", certificates, url) { + + private val password = "changeit" + + override fun createFileContent(): ByteArray { + val keyStore = KeyStore.getInstance("JKS") + keyStore.load(null, password.toCharArray()) + + certificates.forEachIndexed { index, certificate -> + val alias = + certificate.safeCastTo()?.getCn() ?: "server-certificate-$index" + keyStore.setCertificateEntry(alias, certificate) + } + + val outputStream = ByteArrayOutputStream() + outputStream.use { os -> keyStore.store(os, password.toCharArray()) } + return outputStream.toByteArray() + } + + override fun onSuccess(e: AnActionEvent) { + Messages.showInfoMessage( + e.project, + UiToolsBundle.message("server-certificates.export-jks-result", password), + e.presentation.text, + ) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class CopyAsPemToClipboardAction(private val certificates: List) : + AnAction( + UiToolsBundle.message("server-certificates.copy-pem-to-clipboard-action-title"), + null, + AllIcons.Actions.Copy, + ) { + + override fun actionPerformed(e: AnActionEvent) { + val pemFile = toPemFile(certificates) + CopyPasteManager.getInstance().setContents(StringSelection(pemFile)) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ShowAsPemAction( + private val certificates: List, + private val context: DeveloperUiToolContext, + private val configuration: DeveloperToolConfiguration, + private val project: Project?, + private val parentDisposable: Disposable, + private val parentComponent: () -> JComponent, + ) : AnAction(UiToolsBundle.message("server-certificates.show-as-pem-action-title"), null, null) { + + override fun actionPerformed(e: AnActionEvent) { + val content = panel { + row { + cell( + AdvancedEditor( + id = "server-certificates-show-certificates-as-pem", + context = context, + configuration = configuration, + project = project, + title = null, + editorMode = OUTPUT, + parentDisposable = parentDisposable, + ) + .apply { text = toPemFile(certificates) } + .component + ) + .resizableColumn() + .align(Align.FILL) + } + .resizableRow() + } + createPopup(content).show(RelativePoint.getSouthOf(parentComponent()), Balloon.Position.below) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ShowCertificateDetailsAction( + private val certificates: List, + private val context: DeveloperUiToolContext, + private val configuration: DeveloperToolConfiguration, + private val project: Project?, + private val parentDisposable: Disposable, + private val parentComponent: () -> JComponent, + ) : + AnAction( + UiToolsBundle.message("server-certificates.show-certificate-details-action-title"), + null, + null, + ) { + + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = certificates.size == 1 + } + + override fun actionPerformed(e: AnActionEvent) { + val content = panel { + row { + cell( + AdvancedEditor( + id = "server-certificates-show-details", + context = context, + configuration = configuration, + project = project, + title = null, + editorMode = OUTPUT, + parentDisposable = parentDisposable, + ) + .apply { text = certificates[0].toString() } + .component + ) + .resizableColumn() + .align(Align.FILL) + } + .resizableRow() + } + createPopup(content).showInCenterOf(parentComponent()) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private data class HttpResponse( + val certificates: List?, + val protocol: String, + val statusCode: Int, + val statusMessage: String, + val headers: Map>, + val body: String?, + ) + + // -- Inner Type ---------------------------------------------------------- // + + private class CertificateCapturingTrustManager(private val allowInsecureConnection: Boolean) : + X509TrustManager { + val serverCertificates = mutableListOf() + + override fun checkClientTrusted(chain: Array?, authType: String?) { + // Nothing to do + } + + override fun checkServerTrusted(chain: Array?, authType: String?) { + serverCertificates.clear() + chain?.forEach { serverCertificates.add(it) } + + if (!allowInsecureConnection) { + try { + val defaultTrustManager = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + defaultTrustManager.init(null as KeyStore?) + val defaultX509TrustManager = + defaultTrustManager.trustManagers.firstOrNull() as? X509TrustManager + ?: throw IllegalStateException("Default trust manager not available") + defaultX509TrustManager.checkServerTrusted(chain, authType) + } catch (e: Exception) { + throw IllegalStateException("Failed to validate server certificate: ${e.message}", e) + } + } + } + + override fun getAcceptedIssuers(): Array = arrayOf() + + fun createSslContext(): SSLContext = + with(SSLContext.getInstance("TLS")) { + init(null, arrayOf(this@CertificateCapturingTrustManager), SecureRandom()) + this + } + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = UiToolsBundle.message("server-certificates.menu-title"), + contentTitle = UiToolsBundle.message("server-certificates.content-title"), + ) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> ServerCertificates) = { configuration -> + ServerCertificates(project, context, configuration, parentDisposable) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + fun toPemFile(certificates: List) = + certificates + .flatMap { cert -> + listOf( + "-----BEGIN CERTIFICATE-----", + Base64.getMimeEncoder(64, "\n".toByteArray()).encodeToString(cert.encoded), + "-----END CERTIFICATE-----", + ) + } + .joinToString(System.lineSeparator()) + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextDiffViewer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextDiffViewer.kt new file mode 100644 index 00000000..adca401c --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextDiffViewer.kt @@ -0,0 +1,123 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.intellij.diff.DiffContentFactory +import com.intellij.diff.DiffManagerEx +import com.intellij.diff.contents.DiffContent +import com.intellij.diff.requests.SimpleDiffRequest +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.Panel +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation + +class TextDiffViewer( + configuration: DeveloperToolConfiguration, + private val project: Project?, + parentDisposable: Disposable, +) : DeveloperUiTool(parentDisposable) { + // -- Properties ---------------------------------------------------------- // + + private val firstText = configuration.register("firstText", "", INPUT, FIRST_TEXT_EXAMPLE) + private val secondText = configuration.register("secondText", "", INPUT, SECOND_TEXT_EXAMPLE) + + // -- Initialization ------------------------------------------------------ // + + init { + wrapComponentInScrollPane = false + } + + // -- Exposed Methods ----------------------------------------------------- // + + override fun Panel.buildUi() { + row { + val firstDiffContent = createDiffContent(firstText) + val secondDiffContent = createDiffContent(secondText) + val diffComponent = + DiffManagerEx.getInstance() + .createRequestPanel(project, parentDisposable, null) + .apply { + setRequest(SimpleDiffRequest(null, firstDiffContent, secondDiffContent, null, null)) + } + .component + cell(diffComponent).resizableColumn().align(Align.FILL) + } + .resizableRow() + } + + // -- Private Methods ----------------------------------------------------- // + + private fun createDiffContent(textProperty: ValueProperty): DiffContent { + val document = + EditorFactory.getInstance().createDocument(textProperty.get()).apply { + addDocumentListener( + object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + textProperty.set(event.document.text, TEXT_CHANGE_FROM_DOCUMENT_LISTENER) + } + }, + parentDisposable, + ) + } + + textProperty.afterChangeConsumeEvent(parentDisposable) { event -> + if (event.id != TEXT_CHANGE_FROM_DOCUMENT_LISTENER) { + runWriteAction { document.setText(event.newValue) } + } + } + + return DiffContentFactory.getInstance().create(project, document) + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Text Diff", contentTitle = "Text Diff Viewer") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> TextDiffViewer) = { configuration -> + TextDiffViewer( + configuration = configuration, + project = project, + parentDisposable = parentDisposable, + ) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val TEXT_CHANGE_FROM_DOCUMENT_LISTENER = "documentChangeListener" + + private val FIRST_TEXT_EXAMPLE = + """ + The sky is blue, + The sun is shining, + And the birds are singing. + """ + .trimIndent() + + private val SECOND_TEXT_EXAMPLE = + """ + The sky is gray, + The rain is pouring, + And the birds are silent. + """ + .trimIndent() + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/TextStatistic.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextStatistic.kt similarity index 52% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/TextStatistic.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextStatistic.kt index be264a2c..419593eb 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/TextStatistic.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/TextStatistic.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project @@ -9,31 +9,31 @@ import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.panel import com.intellij.util.Alarm -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.SimpleTable -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils.simpleColumnInfo -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.TextStatisticUtils -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other.TextStatistic.OpenTextStatisticContext -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolHandler -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolReference -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import org.apache.commons.text.StringEscapeUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.common.TextStatisticUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.SimpleTable +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.simpleColumnInfo +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolReference +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other.TextStatistic.OpenTextStatisticContext import javax.swing.SortOrder +import org.apache.commons.text.StringEscapeUtils - -internal class TextStatistic( +class TextStatistic( private val context: DeveloperUiToolContext, private val configuration: DeveloperToolConfiguration, private val project: Project?, - parentDisposable: Disposable + parentDisposable: Disposable, ) : DeveloperUiTool(parentDisposable), OpenDeveloperToolHandler { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // private val text = configuration.register("text", "", INPUT, TEXT_EXAMPLE) @@ -48,7 +48,6 @@ internal class TextStatistic( private val sentencesCounter = TextMetric("Sentences") private val averageWordsPerSentenceCounter = TextMetric("Average words per sentence") private val paragraphsCounter = TextMetric("Paragraphs") - private val characterCounter = TextMetric("Characters") private val uniqueCharactersCounter = TextMetric("Unique characters") private val lettersCounter = TextMetric("Letters") private val digitsCounter = TextMetric("Digits") @@ -61,18 +60,21 @@ internal class TextStatistic( private val counterAlarm by lazy { Alarm(parentDisposable) } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // override fun Panel.buildUi() { row { - cell( - Splitter(true, 0.75f).apply { - firstComponent = createInputEditorComponent() - secondComponent = createMetricsComponent() - } - ).align(Align.FILL).resizableColumn() - }.resizableRow() + cell( + Splitter(true, 0.75f).apply { + firstComponent = createInputEditorComponent() + secondComponent = createMetricsComponent() + } + ) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() } override fun afterBuildUi() { @@ -84,70 +86,84 @@ internal class TextStatistic( updateCounter() } - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun createInputEditorComponent() = DeveloperToolEditor( - id = "text", - context = context, - configuration = configuration, - project = project, - title = "Text", - editorMode = DeveloperToolEditor.EditorMode.INPUT, - parentDisposable = parentDisposable, - textProperty = text, - ).apply { - onTextChangeFromUi { - counterAlarm.addRequest({ updateCounter() }, 100) - } - }.component + // -- Private Methods ----------------------------------------------------- // + + private fun createInputEditorComponent() = + AdvancedEditor( + id = "text", + context = context, + configuration = configuration, + project = project, + title = "Text", + editorMode = EditorMode.INPUT, + parentDisposable = parentDisposable, + textProperty = text, + ) + .apply { onTextChangeFromUi { counterAlarm.addRequest({ updateCounter() }, 100) } } + .component private fun createMetricsComponent() = panel { row { - cell(JBTabbedPane().apply { - metricsTable = createMetricsTable() - addTab("Metrics", ScrollPaneFactory.createScrollPane(metricsTable, false)) - - uniqueWordsTable = createUniqueWordsTable() - addTab("Unique Words", ScrollPaneFactory.createScrollPane(uniqueWordsTable, false)) - - uniqueCharactersTable = createUniqueCharactersTable() - addTab("Unique Characters", ScrollPaneFactory.createScrollPane(uniqueCharactersTable, false)) - }).resizableColumn().align(Align.FILL) - }.resizableRow() + cell( + JBTabbedPane().apply { + metricsTable = createMetricsTable() + addTab("Metrics", ScrollPaneFactory.createScrollPane(metricsTable, false)) + + uniqueWordsTable = createUniqueWordsTable() + addTab("Unique Words", ScrollPaneFactory.createScrollPane(uniqueWordsTable, false)) + + uniqueCharactersTable = createUniqueCharactersTable() + addTab( + "Unique Characters", + ScrollPaneFactory.createScrollPane(uniqueCharactersTable, false), + ) + } + ) + .resizableColumn() + .align(Align.FILL) + } + .resizableRow() } - private fun createMetricsTable() = SimpleTable( - items = listOf( - charactersCounter, - wordsCounter, - uniqueWordsCounter, - averageWordLengthCounter, - sentencesCounter, - averageWordsPerSentenceCounter, - paragraphsCounter, - characterCounter, - uniqueCharactersCounter, - lettersCounter, - digitsCounter, - nonAsciiCharactersCounter, - isoControlCharactersCounter, - whitespacesCounter, - lineBreaksCounter - ), - columns = listOf( - simpleColumnInfo("Metric", { it.title }) { it.title }, - simpleColumnInfo("Occurrence", { it.value }) { it.value } - ), - toCopyValue = { "${it.title}: ${it.value}" }, - initialSortedColumn = 0 to SortOrder.UNSORTED - ) - - private fun createUniqueWordsTable(): SimpleTable> = SimpleTable( - items = uniqueWords, - columns = listOf(simpleColumnInfo("Word", { it.first }, { it.first }), simpleColumnInfo("Occurrence", { it.second.toString() }, { it.second })), - toCopyValue = { it.first }, - initialSortedColumn = 1 to SortOrder.DESCENDING - ) + private fun createMetricsTable() = + SimpleTable( + items = + listOf( + charactersCounter, + wordsCounter, + uniqueWordsCounter, + averageWordLengthCounter, + sentencesCounter, + averageWordsPerSentenceCounter, + paragraphsCounter, + uniqueCharactersCounter, + lettersCounter, + digitsCounter, + nonAsciiCharactersCounter, + isoControlCharactersCounter, + whitespacesCounter, + lineBreaksCounter, + ), + columns = + listOf( + simpleColumnInfo("Metric", { it.title }) { it.title }, + simpleColumnInfo("Occurrence", { it.value }) { it.value }, + ), + toCopyValue = { "${it.title}: ${it.value}" }, + initialSortedColumn = 0 to SortOrder.UNSORTED, + ) + + private fun createUniqueWordsTable(): SimpleTable> = + SimpleTable( + items = uniqueWords, + columns = + listOf( + simpleColumnInfo("Word", { it.first }, { it.first }), + simpleColumnInfo("Occurrence", { it.second.toString() }, { it.second }), + ), + toCopyValue = { it.first }, + initialSortedColumn = 1 to SortOrder.DESCENDING, + ) private fun createUniqueCharactersTable(): SimpleTable> { val characterToDisplay: (Pair) -> String = { (character, _) -> @@ -160,25 +176,23 @@ internal class TextStatistic( '\r' -> "\\r" else -> "\\u" + String.format("%04x", character.code) } - } - else if (character == ' ') { + } else if (character == ' ') { "Whitespace" - } - else if (character.code > 127) { + } else if (character.code > 127) { "$character (${StringEscapeUtils.escapeJava(character.toString())})" - } - else { + } else { character.toString() } } return SimpleTable( items = uniqueCharacters, - columns = listOf( - simpleColumnInfo("Character", characterToDisplay, characterToDisplay), - simpleColumnInfo("Occurrence", { it.second.toString() }, { it.second }) - ), + columns = + listOf( + simpleColumnInfo("Character", characterToDisplay, characterToDisplay), + simpleColumnInfo("Occurrence", { it.second.toString() }, { it.second }), + ), toCopyValue = { it.first.toString() }, - initialSortedColumn = 1 to SortOrder.DESCENDING + initialSortedColumn = 1 to SortOrder.DESCENDING, ) } @@ -191,7 +205,6 @@ internal class TextStatistic( sentencesCounter.value = sentencesCount.toString() averageWordsPerSentenceCounter.value = "%.2f".format(averageWordsPerSentence) paragraphsCounter.value = paragraphsCount.toString() - characterCounter.value = charactersCount.toString() uniqueCharactersCounter.value = uniqueCharacters.size.toString() lettersCounter.value = lettersCount.toString() digitsCounter.value = digitsCount.toString() @@ -209,53 +222,52 @@ internal class TextStatistic( uniqueCharactersTable.reload() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class TextMetric(val title: String, var value: String = "Unknown") - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // data class OpenTextStatisticContext(val text: String) : OpenDeveloperToolContext - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { override fun getDeveloperUiToolPresentation() = - DeveloperUiToolPresentation( - menuTitle = "Text Statistic", - contentTitle = "Text Statistic" - ) + DeveloperUiToolPresentation(menuTitle = "Text Statistic", contentTitle = "Text Statistic") override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> TextStatistic) = - { configuration -> - TextStatistic( - context = context, - configuration = configuration, - project = project, - parentDisposable = parentDisposable - ) - } + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> TextStatistic) = { configuration -> + TextStatistic( + context = context, + configuration = configuration, + project = project, + parentDisposable = parentDisposable, + ) + } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { private const val ID = "text-statistic" - private val TEXT_EXAMPLE = """ + private val TEXT_EXAMPLE = + """ Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. It is a paradisematic country, in which roasted parts of sentences fly into your mouth. - """.trimIndent() + """ + .trimIndent() - val openTextStatisticReference = OpenDeveloperToolReference.of(ID, OpenTextStatisticContext::class) + val openTextStatisticReference = + OpenDeveloperToolReference.of(ID, OpenTextStatisticContext::class) } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/Unarchiver.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/Unarchiver.kt new file mode 100644 index 00000000..60f56b78 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/other/Unarchiver.kt @@ -0,0 +1,1978 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other + +import com.intellij.icons.AllIcons +import com.intellij.ide.BrowserUtil +import com.intellij.ide.CommonActionsManager +import com.intellij.ide.util.treeView.NodeRenderer +import com.intellij.notification.NotificationType +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataProvider +import com.intellij.openapi.actionSystem.DefaultActionGroup +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.ide.CopyPasteManager +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.NlsActions +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.ui.AnimatedIcon +import com.intellij.ui.DoubleClickListener +import com.intellij.ui.FilteringTree +import com.intellij.ui.HyperlinkLabel +import com.intellij.ui.JBColor +import com.intellij.ui.ScrollPaneFactory +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.awt.RelativePoint +import com.intellij.ui.components.JBLabel +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.LabelPosition +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindSelected +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.table.JBTable +import com.intellij.ui.tree.TreeVisitor +import com.intellij.ui.treeStructure.Tree +import com.intellij.util.PlatformIcons +import com.intellij.util.SystemProperties +import com.intellij.util.io.URLUtil +import com.intellij.util.text.DateFormatUtil +import com.intellij.util.ui.UIUtil +import com.intellij.util.ui.components.BorderLayoutPanel +import com.intellij.util.ui.tree.TreeUtil +import dev.turingcomplete.intellijdevelopertoolsplugin.common.CopyValuesAction +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginCommonDataKeys.SELECTED_VALUES +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.common.extension +import dev.turingcomplete.intellijdevelopertoolsplugin.common.nameWithoutExtension +import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.common.toLowerCasePreservingASCIIRules +import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.ChangeListener +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.ResetListener +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.FileDropHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.NotificationUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.createContextMenuMouseListener +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils.createToggleAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolReference +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.frame.instance.handling.OpenDeveloperToolService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other.Unarchiver.OpenUnarchiverContext +import java.awt.datatransfer.StringSelection +import java.awt.dnd.DropTarget +import java.awt.event.MouseEvent +import java.io.BufferedInputStream +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption.CREATE_NEW +import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING +import java.nio.file.StandardOpenOption.WRITE +import java.nio.file.attribute.BasicFileAttributeView +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileTime +import java.util.zip.ZipEntry +import javax.swing.Icon +import javax.swing.JComponent +import javax.swing.JTree +import javax.swing.SwingConstants +import javax.swing.event.HyperlinkEvent +import javax.swing.table.DefaultTableModel +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel +import javax.swing.tree.TreePath +import org.apache.commons.compress.archivers.ArchiveEntry +import org.apache.commons.compress.archivers.ArchiveInputStream +import org.apache.commons.compress.archivers.ArchiveStreamFactory +import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry +import org.apache.commons.compress.archivers.sevenz.SevenZFile +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipMethod +import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream +import org.apache.commons.compress.compressors.gzip.GzipUtils +import org.apache.commons.io.FileUtils +import org.apache.commons.io.IOUtils + +class Unarchiver( + private val project: Project?, + private val configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, +) : + DeveloperUiTool(parentDisposable), + DataProvider, + ChangeListener, + ResetListener, + OpenDeveloperToolHandler { + // -- Properties ---------------------------------------------------------- // + + private val archiveTreeSortingMode = + configuration.register("archiveTreeSortingMode", DEFAULT_SORTING_MODE) + private val showArchiveNodeUncompressedSize = + configuration.register("showArchiveNodeUncompressedSize", DEFAULT_SHOW_ARCHIVE_NODE_SIZE) + private val showArchiveNodeTotalNumberOfChildren = + configuration.register( + "showArchiveNodeTotalNumberOfChildren", + DEFAULT_SHOW_ARCHIVE_NODE_TOTAL_NUMBER_OF_CHILDREN, + ) + private val openFileInEditorOnDoubleClick = + configuration.register("openFileInEditorOnDoubleClick", DEFAULT_OPEN_IN_EDITOR_ON_DOUBLE_CLICK) + private val lastSelectedOpenedDirectoryPath = + configuration.register("lastSelectedOpenedDirectoryPath", DEFAULT_LAST_OPENED_DIRECTORY_PATH) + private val lastSelectedTargetDirectoryPath = + configuration.register( + "lastSelectedTargetDirectoryPath", + DEFAULT_LAST_SELECTED_TARGET_DIRECTORY_PATH, + ) + private val createArchiveFilenameSubDirectory = + configuration.register( + "createArchiveFilenameSubDirectory", + DEFAULT_CREATE_ARCHIVE_FILENAME_SUB_DIRECTORY, + ) + private val clearTargetDirectory = + configuration.register("clearTargetDirectory", DEFAULT_CLEAR_TARGET_DIRECTORY) + private val createParentDirectories = + configuration.register("createParentDirectories", DEFAULT_CREATE_PARENT_DIRECTORIES) + private val preserveDirectoryStructure = + configuration.register("preserveDirectoryStructure", DEFAULT_PRESERVE_DIRECTORY_STRUCTURE) + private val preserveFileAttributes = + configuration.register("preserveFileAttributes", DEFAULT_PRESERVE_FILE_ATTRIBUTES) + private val openTargetDirectoryAfterExtraction = + configuration.register( + "openTargetDirectoryAfterExtraction", + DEFAULT_OPEN_TARGET_DIRECTORY_AFTER_EXTRACTION, + ) + + private var tree: ArchiveTree? = null + private val content = BorderLayoutPanel() + private val noArchiveFilePanel by lazy { createNoArchiveFilePanel() } + private val readingArchiveFilePanel by lazy { createReadingArchiveFilePanel() } + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun Panel.buildUi() { + content.dropTarget = DropTarget(content, FileDropHandler(project, openArchiveFile())) + content.addToCenter(noArchiveFilePanel) + + row { cell(content).resizableColumn().align(Align.FILL) }.resizableRow() + } + + override fun afterBuildUi() { + configuration.addChangeListener(parentDisposable, this) + configuration.addResetListener(parentDisposable, this) + } + + override fun configurationReset() { + closeArchiveFile() + } + + override fun configurationChanged(property: ValueProperty) { + tree?.let { + val treeModel = it.tree.model as DefaultTreeModel + + if (property == archiveTreeSortingMode) { + doSortArchiveTree((treeModel.root as DefaultMutableTreeNode).userObject as DirectoryNode) + } + + val expandedPaths = TreeUtil.collectExpandedPaths(it.tree) + treeModel.reload() + it.searchModel.updateStructure() + TreeUtil.restoreExpandedPaths(it.tree, expandedPaths) + } + } + + /** + * This method may get called before the [tree] was initialized, for example, during a drop of a + * file. + */ + override fun getData(dataId: String): Any? = + when { + SELECTED_VALUES.`is`(dataId) -> tree?.getSelectedArchiveNodes() + else -> super.getData(dataId) + } + + override fun applyOpenDeveloperToolContext(context: OpenUnarchiverContext) { + openArchiveFile().invoke(context.archiveFilePath) + } + + // -- Private Methods ----------------------------------------------------- // + + private fun replaceContent(centerContent: JComponent) { + content.removeAll() + content.addToCenter(centerContent) + content.revalidate() + content.repaint() + } + + private fun openArchiveFile(): (Path) -> Unit = { archiveFilePath -> + lastSelectedOpenedDirectoryPath.set(archiveFilePath.parent.toString()) + + val previousExpandedRelativePaths = mutableSetOf() + ApplicationManager.getApplication().invokeAndWait { + tree?.let { tree -> + TreeUtil.collectExpandedPaths(tree.tree) + .mapNotNull { treePath -> ArchiveNode.findInLastPathComponent(treePath) } + .forEach { previousExpandedRelativePaths.add(it.relativePath) } + } + + replaceContent(readingArchiveFilePanel) + } + + ApplicationManager.getApplication().executeOnPooledThread { + try { + val rootNode = buildArchiveTree(archiveFilePath) + doSortArchiveTree(rootNode) + + ApplicationManager.getApplication().invokeLater { + replaceContent(createArchiveTreePanel(archiveFilePath, rootNode)) + + if (previousExpandedRelativePaths.isNotEmpty()) { + TreeUtil.promiseExpand(tree!!.tree) { treePath -> + val archiveNode = ArchiveNode.findInLastPathComponent(treePath) + if ( + archiveNode != null && + previousExpandedRelativePaths.contains(archiveNode.relativePath) + ) { + TreeVisitor.Action.CONTINUE + } else { + TreeVisitor.Action.SKIP_CHILDREN + } + } + } + } + } catch (e: Exception) { + log.warn("Reading archive file failed", e) + ApplicationManager.getApplication().invokeLater { + replaceContent(noArchiveFilePanel) + Messages.showErrorDialog( + project, + "Reading archive failed: ${e.message}", + "Reading Archive Failed", + ) + } + } + } + } + + private fun buildArchiveTree(archiveFilePath: Path) = + RootNode(archiveFilePath).apply { + val normalizedPathToDirectoryNode = mutableMapOf() + normalizedPathToDirectoryNode[""] = this + iterateEntries { archiveEntry, _ -> + val path = archiveEntry.name + val normalizedPath = path.trimEnd('/') + val parentNode: DirectoryNode = + buildParentDirectoryTreeStructure(this, normalizedPath, normalizedPathToDirectoryNode) { + it.totalChildren += 1 + it.addUncompressedSize(archiveEntry.size) + } + val name = normalizedPath.substringAfterLast("/", normalizedPath) + val isDirectory = path.endsWith("/") + if (!isDirectory) { + val fileNode = FileNode(name, archiveEntry) + parentNode.children.add(fileNode) + } else { + if (!normalizedPathToDirectoryNode.containsKey(normalizedPath)) { + val directoryNode = DirectoryNode(name, path, archiveEntry) + normalizedPathToDirectoryNode[normalizedPath] = directoryNode + parentNode.children.add(directoryNode) + } else { + normalizedPathToDirectoryNode[normalizedPath]!!.archiveEntry = archiveEntry + } + } + true + } + } + + private fun buildParentDirectoryTreeStructure( + rootNode: DirectoryNode, + normalizedPath: String, + normalizedPathToDirectoryNode: MutableMap, + modifyParentNode: (DirectoryNode) -> Unit, + ): DirectoryNode { + val parentPathParts = normalizedPath.split("/") + var currentPath = "" + var parentNode: DirectoryNode = rootNode + for (i in parentPathParts.indices) { + val currentNormalizedPath = currentPath.trimEnd('/') + parentNode = + normalizedPathToDirectoryNode.computeIfAbsent(currentNormalizedPath) { + val directoryNode = + DirectoryNode( + currentNormalizedPath.substringAfterLast("/", currentNormalizedPath), + currentPath, + null, + ) + parentNode.children.add(directoryNode) + directoryNode + } + modifyParentNode(parentNode) + currentPath += parentPathParts[i] + "/" + } + return parentNode + } + + private fun doSortArchiveTree(node: DirectoryNode) { + node.children.filterIsInstance().forEach { doSortArchiveTree(it) } + node.children.sortWith(archiveTreeSortingMode.get().comparator) + } + + private fun createArchiveTreePanel(archiveFilePath: Path, rootNode: RootNode) = panel { + val archiveTree = ArchiveTree(rootNode) + tree = archiveTree + + row { + cell(archiveTree.installSearchField()).gap(RightGap.SMALL) + + val actionGroup = + DefaultActionGroup().apply { + val commonActionsManager = CommonActionsManager.getInstance() + add(commonActionsManager.createExpandAllHeaderAction(archiveTree.tree)) + add(commonActionsManager.createCollapseAllHeaderAction(archiveTree.tree)) + addSeparator() + add(createSettingsActionGroup()) + add(createReloadAction(archiveFilePath)) + addSeparator() + add( + ChooseArchiveFileToOpenAction( + project, + archiveTree.tree, + lastSelectedOpenedDirectoryPath, + openArchiveFile(), + ) + ) + } + cell( + ActionManager.getInstance() + .createActionToolbar(Unarchiver::class.qualifiedName!!, actionGroup, true) + .run { + targetComponent = component + component + } + ) + } + .bottomGap(BottomGap.NONE) + + row { + cell(ScrollPaneFactory.createScrollPane(archiveTree.component, false)) + .align(Align.FILL) + .resizableColumn() + } + .resizableRow() + .topGap(TopGap.NONE) + } + + private fun createSettingsActionGroup() = + object : DefaultActionGroup("Settings", true) { + + init { + templatePresentation.icon = AllIcons.General.GearPlain + + add( + DefaultActionGroup("Sorting", true).apply { + SortingMode.entries.forEach { + add( + createToggleAction( + it.title, + { archiveTreeSortingMode.get() == it }, + { state -> if (state) archiveTreeSortingMode.set(it) }, + ) + ) + } + } + ) + + add( + createToggleAction( + "Show uncompressed size", + { showArchiveNodeUncompressedSize.get() }, + { showArchiveNodeUncompressedSize.set(it) }, + ) + ) + add( + createToggleAction( + "Show total number of children", + { showArchiveNodeTotalNumberOfChildren.get() }, + { showArchiveNodeTotalNumberOfChildren.set(it) }, + ) + ) + + addSeparator() + + add( + createToggleAction( + "Open file in editor on double click", + { openFileInEditorOnDoubleClick.get() }, + { openFileInEditorOnDoubleClick.set(it) }, + ) + ) + + addSeparator() + + add(createCloseArchiveFileAction()) + } + + @Suppress("UnstableApiUsage") + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = UIUtil.isShowing(tree!!.tree) + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + } + + private fun createReloadAction(archiveFilePath: Path): AnAction = + object : DumbAwareAction("Reload", null, AllIcons.Actions.Refresh) { + + override fun actionPerformed(e: AnActionEvent) { + openArchiveFile().invoke(archiveFilePath) + } + + @Suppress("UnstableApiUsage") + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = UIUtil.isShowing(tree!!.tree) + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + } + + private fun createCloseArchiveFileAction(): AnAction = + object : DumbAwareAction("Close Archive File") { + + override fun actionPerformed(e: AnActionEvent) { + closeArchiveFile() + } + + @Suppress("UnstableApiUsage") + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = UIUtil.isShowing(tree!!.tree) + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + } + + private fun closeArchiveFile() { + content.removeAll() + content.addToCenter(noArchiveFilePanel) + content.revalidate() + content.repaint() + } + + private fun createNoArchiveFilePanel(): JComponent { + val actionsPanel = panel { + row { + cell( + ChooseArchiveFileToOpenAction( + project, + null, + lastSelectedOpenedDirectoryPath, + openArchiveFile(), + ) + .toHyperlinkLabel() + ) + } + row { + cell( + JBLabel( + "Drop archive file here to open", + AllIcons.Actions.Download, + SwingConstants.LEFT, + ) + ) + } + .bottomGap(BottomGap.MEDIUM) + row { + lateinit var supportedTypesLink: JComponent + supportedTypesLink = + link("Supported archives and known limitations") { + val text = + """ +

    Supported archive types: ${supportedArchiveExtensions.sorted().joinToString(", ")}

    +

    The Apache Commons Compress 1.26.0 library is used for the underlining archive handling,
    which has some known limitations (e.g., encryption is not supported).

    + """ + .trimIndent() + JBPopupFactory.getInstance() + .createHtmlTextBalloonBuilder(text, null, UIUtil.getPanelBackground()) { e -> + if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) { + BrowserUtil.browse(e.url) + } + } + .setDialogMode(true) + .setBorderColor(JBColor.border()) + .setBlockClicksThroughBalloon(true) + .setRequestFocus(true) + .setCloseButtonEnabled(true) + .createBalloon() + .apply { + setAnimationEnabled(false) + show(RelativePoint.getSouthOf(supportedTypesLink), Balloon.Position.below) + } + } + .component + } + } + return panel { row { cell(actionsPanel).align(Align.CENTER) }.resizableRow() } + } + + private fun createReadingArchiveFilePanel() = panel { + row { + cell(JBLabel("Reading archive...").apply { icon = AnimatedIcon.Default.INSTANCE }) + .align(Align.CENTER) + } + .resizableRow() + } + + // -- Inner Type ---------------------------------------------------------- // + + private inner class ArchiveTree(private val rootNode: RootNode) : + FilteringTree(Tree(), DefaultMutableTreeNode(rootNode)) { + + init { + tree.cellRenderer = + ArchiveTreeNodeRenderer( + showArchiveNodeUncompressedSize = showArchiveNodeUncompressedSize, + showArchiveNodeTotalNumberOfChildren = showArchiveNodeTotalNumberOfChildren, + ) + + tree.addMouseListener( + createContextMenuMouseListener(Unarchiver::class.java.simpleName) { mouseEvent -> + DefaultActionGroup().apply { + add(createExtractAction()) + add(createFactExtractAction()) + + addSeparator() + + add(createOpenInEditorAction()) + add(createOpenWithDefaultApplicationAction()) + add(createOpenEnclosingDirectoryAction()) + add(createCopyContentToClipboardAction()) + + addSeparator() + + add(createShowSelectedElementDetailsAction(mouseEvent)) + + addSeparator() + + add( + DefaultActionGroup("Copy", true).apply { + templatePresentation.icon = PlatformIcons.COPY_ICON + add( + CopyValuesAction( + "Filename", + { "$it Filenames" }, + { (it as ArchiveNode).fileName }, + null, + ) + ) + add( + CopyValuesAction( + "Relative Path", + { "$it Relative Paths" }, + { (it as ArchiveNode).relativePath()?.toString() }, + null, + ) + ) + add( + CopyValuesAction( + "Absolute Path", + { "$it Absolute Paths" }, + { (it as ArchiveNode).absolutePath(rootNode.archiveFilePath)?.toString() }, + null, + ) + ) + } + ) + } + } + ) + + if (project != null) { + object : DoubleClickListener() { + + override fun onDoubleClick(e: MouseEvent): Boolean { + if (!openFileInEditorOnDoubleClick.get()) { + return false + } + openInEditor(getSelectedArchiveNodes().filterIsInstance(), project, false) + return true + } + } + .installOn(tree) + } + } + + private fun createCopyContentToClipboardAction() = + ArchiveNodeAction("Copy Content to Clipboard", { it is FileNode && it.isTextFile() }) { + archiveNode, + _ -> + ApplicationManager.getApplication().executeOnPooledThread { + try { + val content = rootNode.readEntry(archiveNode.archiveEntry!!) + CopyPasteManager.getInstance() + .setContents(StringSelection(content.toString(StandardCharsets.UTF_8))) + } catch (e: Exception) { + log.warn("Failed to read archive entry: ${archiveNode.archiveEntry!!.name}", e) + ApplicationManager.getApplication().invokeLater { + Messages.showErrorDialog( + project, + "Failed to read archive entry: ${e.message}", + "Copy Content to Clipboard Failed", + ) + } + } + } + } + + override fun getNodeClass(): Class = + DefaultMutableTreeNode::class.java + + override fun createNode(archiveNode: ArchiveNode): DefaultMutableTreeNode = + DefaultMutableTreeNode(archiveNode) + + override fun getChildren(archiveNode: ArchiveNode): Iterable = + if (archiveNode is DirectoryNode) archiveNode.children else emptyList() + + override fun getText(archiveNode: ArchiveNode?): String? = archiveNode?.fileName + + private fun createExtractAction() = + ArchiveNodesAction("Extract...", { true }) { archiveNodes, _ -> + ApplicationManager.getApplication().executeOnPooledThread { + val archiveNodesToExtract = determineArchiveNodesToExtract(archiveNodes) + if (archiveNodesToExtract.archiveNodes.isEmpty()) { + log.info("No entries to extract found") + return@executeOnPooledThread + } + + ApplicationManager.getApplication().invokeLater { + showExtractionDialog(rootNode, archiveNodesToExtract) { extractionContext -> + ApplicationManager.getApplication().executeOnPooledThread { + ExtractTask(project, extractionContext).queue() + } + } + } + } + } + + private fun createFactExtractAction() = + ArchiveNodesAction( + "Fast Extract", + { true }, + "Extracts the selected entry into a temporary directory.", + ) { archiveNodes, _ -> + ApplicationManager.getApplication().executeOnPooledThread { + val archiveNodesToExtract = determineArchiveNodesToExtract(archiveNodes) + if (archiveNodesToExtract.archiveNodes.isEmpty()) { + log.info("No entries to extract found") + return@executeOnPooledThread + } + + val tempDirectory = + Files.createTempDirectory("${rootNode.fileName.substringBeforeLast(".")}-") + tempDirectory.toFile().deleteOnExit() + + ExtractTask( + project = project, + extractionContext = + ExtractionContext( + rootNode = rootNode, + archiveNodes = archiveNodesToExtract.archiveNodes, + targetDirectoryPath = tempDirectory, + clearTargetDirectory = true, + preserveDirectoryStructure = true, + preserveFileAttributes = true, + createParentDirectories = false, + openTargetDirectoryAfterExtraction = true, + ), + ) + .queue() + } + } + + private fun createOpenEnclosingDirectoryAction() = + ArchiveNodeAction("Open Enclosing Directory...", { it is RootNode }) { rootNode, _ -> + BrowserUtil.browse((rootNode as RootNode).archiveFilePath.parent) + } + + private fun createOpenInEditorAction() = + ArchiveNodesAction("Open in Editor...", { it is FileNode }) { archiveNodes, e -> + val project = + e.dataContext.getData(CommonDataKeys.PROJECT) + ?: throw IllegalStateException("snh: Data missing") + openInEditor(archiveNodes.map { it as FileNode }, project, true) + } + + private fun openInEditor( + archiveNodes: List, + project: Project, + notifyOnError: Boolean, + ) { + ApplicationManager.getApplication().executeOnPooledThread { + val fileEditorManager = FileEditorManager.getInstance(project) + val virtualFileManager = VirtualFileManager.getInstance() + val virtualFiles = + archiveNodes.mapNotNull { + virtualFileManager.findFileByUrl("jar://${it.absolutePath(rootNode.archiveFilePath)}") + } + ApplicationManager.getApplication().invokeLater { + virtualFiles.forEach { + val openEditor = fileEditorManager.openEditor(OpenFileDescriptor(project, it), true) + if (openEditor.isEmpty() && notifyOnError) { + Messages.showErrorDialog( + project, + "Unable to open file '${it.name}' in editor.", + "Open in Editor Failed", + ) + } + } + } + } + } + + private fun createOpenWithDefaultApplicationAction() = + ArchiveNodeAction("Open With Default Application...", { it is RootNode }) { rootNode, _ -> + BrowserUtil.browse((rootNode as RootNode).archiveFilePath) + } + + private fun createShowSelectedElementDetailsAction(contextMenuMouseEvent: MouseEvent) = + object : DumbAwareAction("Show Details...") { + + override fun update(e: AnActionEvent) { + val selectedValues = + SELECTED_VALUES.getData(e.dataContext) + ?: throw IllegalStateException("snh: Data missing") + + e.presentation.isVisible = + selectedValues.singleOrNull()?.let { + it is RootNode || (it is ArchiveNode && it.archiveEntry != null) + } == true + } + + override fun actionPerformed(e: AnActionEvent) { + val selectedValues = + SELECTED_VALUES.getData(e.dataContext) + ?: throw IllegalStateException("snh: Data missing") + val archiveNode = selectedValues.singleOrNull()?.uncheckedCastTo(ArchiveNode::class) + if (archiveNode !is RootNode && archiveNode?.archiveEntry == null) { + return + } + + val (title, panel) = + when (archiveNode) { + is RootNode -> + archiveNode.archiveFilePath.fileName.toString() to + createArchiveFileDetails(archiveNode) + else -> + "${archiveNode.fileName} (${if (archiveNode is DirectoryNode) "directory" else "file"})" to + createArchiveEntryDetails(archiveNode) + } + JBPopupFactory.getInstance() + .createBalloonBuilder(panel) + .setTitle(title) + .setDialogMode(true) + .setFillColor(UIUtil.getPanelBackground()) + .setBorderColor(JBColor.border()) + .setBlockClicksThroughBalloon(true) + .setRequestFocus(true) + .setCloseButtonEnabled(true) + .createBalloon() + .apply { + setAnimationEnabled(false) + show(RelativePoint(contextMenuMouseEvent.locationOnScreen), Balloon.Position.below) + } + } + + private fun createArchiveEntryDetails(archiveNode: ArchiveNode) = panel { + val archiveEntry = archiveNode.archiveEntry!! + + if (archiveNode is DirectoryNode) { + row { + label("Number of direct entries:") + label(archiveNode.children.size.toString()) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Number of all entries:") + label(archiveNode.totalChildren.toString()) + } + .layout(RowLayout.PARENT_GRID) + } + + row { + label( + "Uncompressed size${if (archiveNode is DirectoryNode) " of all entries" else ""}:" + ) + label(StringUtil.formatFileSize(archiveNode.totalUncompressedSize() ?: 0)) + } + .layout(RowLayout.PARENT_GRID) + .bottomGap(BottomGap.NONE) + if (archiveNode.inaccurateTotalUncompressedSize) { + row { + label( + "The uncompressed size may be inaccurate because some entries do not provide size information." + ) + } + .topGap(TopGap.NONE) + } + + if (archiveEntry is ZipArchiveEntry) { + if (archiveNode is FileNode) { + row { + label("Compressed size:") + label(StringUtil.formatFileSize(archiveEntry.compressedSize)) + } + .layout(RowLayout.PARENT_GRID) + } + + row { + label("Method:") + val method = ZipMethod.getMethodByCode(archiveEntry.method) + label(method.name.split("_").joinToString(" ")) + } + .layout(RowLayout.PARENT_GRID) + } + + val fileTimes = FileTimes.fromArchiveEntry(archiveEntry) + row { + label("Creation time:") + label( + if (fileTimes.creationTime != null) + DateFormatUtil.formatDateTime(fileTimes.creationTime.toMillis()) + else "Unknown" + ) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Last modified time:") + label( + if (fileTimes.lastModifiedTime != null) + DateFormatUtil.formatDateTime(fileTimes.lastModifiedTime.toMillis()) + else "Unknown" + ) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Last access time:") + label( + if (fileTimes.lastAccessTime != null) + DateFormatUtil.formatDateTime(fileTimes.lastAccessTime.toMillis()) + else "Unknown" + ) + } + .layout(RowLayout.PARENT_GRID) + + if (archiveEntry is ZipEntry) { + row { + label("Comment:") + label(archiveEntry.comment) + } + .layout(RowLayout.PARENT_GRID) + } + + if (archiveEntry is ZipEntry && archiveEntry.extra != null) { + row { + label("Extra data size:") + label(StringUtil.formatFileSize(archiveEntry.extra.size.toLong())) + } + .layout(RowLayout.PARENT_GRID) + } + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + } + + private fun createArchiveFileDetails(rootNode: RootNode) = + try { + panel { + row { + label("Number of direct entries:") + label(rootNode.children.size.toString()) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Number of all entries:") + label(rootNode.totalChildren.toString()) + } + .layout(RowLayout.PARENT_GRID) + val zipFileAttributes = + Files.readAttributes(rootNode.archiveFilePath, BasicFileAttributes::class.java) + row { + label("Actual size on disk:") + label(FileUtils.byteCountToDisplaySize(zipFileAttributes.size())) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Uncompressed size of all entries:") + label(StringUtil.formatFileSize(rootNode.totalUncompressedSize() ?: 0)) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Creation time:") + label(DateFormatUtil.formatDateTime(zipFileAttributes.creationTime().toMillis())) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Last modified time:") + label(DateFormatUtil.formatDateTime(zipFileAttributes.lastModifiedTime().toMillis())) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Last access time:") + label(DateFormatUtil.formatDateTime(zipFileAttributes.lastAccessTime().toMillis())) + } + .layout(RowLayout.PARENT_GRID) + row { + label("Owner:") + label(Files.getOwner(rootNode.archiveFilePath).name) + } + .layout(RowLayout.PARENT_GRID) + row { + val writable = Files.isWritable(rootNode.archiveFilePath) + icon(if (writable) AllIcons.Ide.Readwrite else AllIcons.Ide.Readonly) + .gap(RightGap.SMALL) + label(if (writable) "File is writeable" else "File is readonly") + } + } + } catch (e: IOException) { + log.warn("Failed to read parameters of file: ${rootNode.archiveFilePath}", e) + panel { row { label("Failed to read file attributes: ${e.message}").bold() } } + } + + fun getSelectedArchiveNodes(): List = + TreeUtil.collectSelectedPaths(tree) + .mapNotNull { + it.lastPathComponent + ?.uncheckedCastTo(DefaultMutableTreeNode::class) + ?.userObject + ?.uncheckedCastTo(ArchiveNode::class) + } + .toList() + + private fun showExtractionDialog( + rootNode: RootNode, + archiveNodesToExtract: ArchiveNodesToExtract, + okCallback: (ExtractionContext) -> Unit, + ) { + object : DialogWrapper(project, tree, false, IdeModalityType.IDE) { + + init { + title = "Extract" + setSize(600, 400) + isModal = true + setOKButtonText("Extract") + init() + } + + override fun doOKAction() { + okCallback( + ExtractionContext( + rootNode = rootNode, + archiveNodes = archiveNodesToExtract.archiveNodes, + targetDirectoryPath = + if (createArchiveFilenameSubDirectory.get()) { + Paths.get(lastSelectedTargetDirectoryPath.get()) + .resolve(rootNode.archiveFilePath.nameWithoutExtension()) + } else { + Paths.get(lastSelectedTargetDirectoryPath.get()) + }, + clearTargetDirectory = clearTargetDirectory.get(), + preserveDirectoryStructure = preserveDirectoryStructure.get(), + preserveFileAttributes = preserveFileAttributes.get(), + createParentDirectories = createParentDirectories.get(), + openTargetDirectoryAfterExtraction = openTargetDirectoryAfterExtraction.get(), + ) + ) + super.doOKAction() + } + + override fun createCenterPanel(): JComponent = panel { + row { + textFieldWithBrowseButton( + FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle("Select Target Directory"), + project, + ) + .label("Target directory:") + .bindText(lastSelectedTargetDirectoryPath) + .resizableColumn() + .align(Align.FILL) + } + row { + checkBox( + "Create a sub-directory with the archive name '${rootNode.archiveFilePath.nameWithoutExtension()}'" + ) + .bindSelected(createArchiveFilenameSubDirectory) + } + row { checkBox("Clear target directory").bindSelected(clearTargetDirectory) } + .bottomGap(BottomGap.SMALL) + + row { + checkBox("Preserve directory structure") + .bindSelected(preserveDirectoryStructure) + .gap(RightGap.SMALL) + contextHelp( + "If unselected, all files will be extracted as a flat list, ignoring the directory structure." + ) + } + row { + checkBox("Create parent directories") + .bindSelected(createParentDirectories) + .enabledIf(preserveDirectoryStructure) + .gap(RightGap.SMALL) + contextHelp( + "For example, if selected, for the path first/second/third/ both parent directories first/second/ will be created, otherwise only the directory third/." + ) + } + row { checkBox("Preserve file attributes").bindSelected(preserveFileAttributes) } + .bottomGap(BottomGap.SMALL) + + row { + checkBox("Open target directory after extraction") + .bindSelected(openTargetDirectoryAfterExtraction) + } + .bottomGap(BottomGap.SMALL) + + row { + val entriesToExtractTable = + JBTable( + DefaultTableModel( + archiveNodesToExtract.displayPathsWithUncompressedSize + .map { (displayPath, totalUncompressedSize) -> + arrayOf( + displayPath, + if (totalUncompressedSize != null) + StringUtil.formatFileSize(totalUncompressedSize) + else "Unknown", + ) + } + .toTypedArray(), + arrayOf("Path", "Uncompressed Size"), + ) + ) + cell(ScrollPaneFactory.createScrollPane(entriesToExtractTable)) + .resizableColumn() + .align(Align.FILL) + .label("Entries to extract:", LabelPosition.TOP) + } + .resizableRow() + .bottomGap(BottomGap.NONE) + row { + comment( + "Expected size on disk: ${if (archiveNodesToExtract.inaccurateTotalUncompressedSize) "+" else ""}${ + StringUtil.formatFileSize( + archiveNodesToExtract.totalUncompressedSize + ) + }" + ) + } + .topGap(TopGap.SMALL) + } + } + .show() + } + + private fun determineArchiveNodesToExtract( + archiveNodes: List + ): ArchiveNodesToExtract { + val rootNode = archiveNodes.find { it is RootNode } + return if (rootNode != null) { + ArchiveNodesToExtract( + archiveNodes = listOf(rootNode), + displayPathsWithUncompressedSize = + (rootNode as RootNode).children.associate { + it.relativePath to it.totalUncompressedSize() + }, + totalUncompressedSize = rootNode.totalUncompressedSize() ?: 0, + inaccurateTotalUncompressedSize = rootNode.inaccurateTotalUncompressedSize, + ) + } else { + val finalArchiveNodes = mutableListOf() + val displayPathsWithUncompressedSize = mutableMapOf() + val relativePathToArchiveNode = archiveNodes.associateBy { it.relativePath }.toSortedMap() + var previousRelativePath: String? = null + var totalUncompressedSize = 0L + var inaccurateTotalUncompressedSize = false + for ((relativePath, archiveNode) in relativePathToArchiveNode) { + if (previousRelativePath == null || !relativePath.startsWith(previousRelativePath)) { + finalArchiveNodes.add(archiveNode) + val totalUncompressedSize0 = archiveNode.totalUncompressedSize() + displayPathsWithUncompressedSize[archiveNode.relativePath] = totalUncompressedSize0 + if (totalUncompressedSize0 != null) { + totalUncompressedSize += totalUncompressedSize0 + } else { + inaccurateTotalUncompressedSize = true + } + previousRelativePath = relativePath + } + } + ArchiveNodesToExtract( + archiveNodes = finalArchiveNodes, + displayPathsWithUncompressedSize = displayPathsWithUncompressedSize, + totalUncompressedSize = totalUncompressedSize, + inaccurateTotalUncompressedSize = inaccurateTotalUncompressedSize, + ) + } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private data class FileTimes( + val lastModifiedTime: FileTime?, + val lastAccessTime: FileTime?, + val creationTime: FileTime?, + ) { + + companion object { + + fun fromArchiveEntry(archiveEntry: ArchiveEntry): FileTimes { + val (lastModifiedTime, lastAccessTime, creationTime) = + when (archiveEntry) { + is ZipEntry -> + Triple( + archiveEntry.lastModifiedTime, + archiveEntry.lastAccessTime, + archiveEntry.creationTime, + ) + is SevenZArchiveEntry -> + Triple( + if (archiveEntry.hasLastModifiedDate) archiveEntry.lastModifiedTime else null, + if (archiveEntry.hasAccessDate) archiveEntry.accessTime else null, + if (archiveEntry.hasCreationDate) archiveEntry.creationTime else null, + ) + + else -> + Triple( + archiveEntry.lastModifiedDate?.toInstant()?.let { FileTime.from(it) }, + null, + null, + ) + } + return FileTimes(lastModifiedTime, lastAccessTime, creationTime) + } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ArchiveNodesAction( + @NlsActions.ActionText title: String, + private val filter: (Any) -> Boolean, + @NlsActions.ActionDescription description: String? = null, + private val actionPerformed: (List, AnActionEvent) -> Unit, + ) : DumbAwareAction(title, description, null) { + + override fun update(e: AnActionEvent) { + val selectedValues = + SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") + + e.presentation.isVisible = selectedValues.any(filter) + } + + override fun actionPerformed(e: AnActionEvent) { + val selectedValues = + SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") + val archiveNodes = selectedValues.filter(filter).map { it as ArchiveNode }.toList() + actionPerformed(archiveNodes, e) + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ArchiveNodeAction( + @NlsActions.ActionText title: String, + private val filter: (ArchiveNode) -> Boolean, + private val actionPerformed: (ArchiveNode, AnActionEvent) -> Unit, + ) : DumbAwareAction(title) { + + override fun update(e: AnActionEvent) { + val archiveNode = getSelectedArchiveNode(e) + e.presentation.isVisible = archiveNode != null + } + + override fun actionPerformed(e: AnActionEvent) { + val archiveNode = getSelectedArchiveNode(e) ?: return + actionPerformed(archiveNode, e) + } + + private fun getSelectedArchiveNode(e: AnActionEvent): ArchiveNode? { + val selectedValues = + SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") + return selectedValues.singleOrNull()?.uncheckedCastTo(ArchiveNode::class)?.takeIf(filter) + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + } + + // -- Inner Type ---------------------------------------------------------- // + + private abstract class ArchiveNode( + val fileName: String, + val icon: Icon, + private var totalUncompressedSize: Long = 0, + /** + * Must be a non-normalized path to distinguish between a directory and a file with the same + * name. + */ + val relativePath: String, + var archiveEntry: ArchiveEntry?, + ) { + + var inaccurateTotalUncompressedSize = false + private set + + fun addUncompressedSize(size: Long) { + if (size != ArchiveEntry.SIZE_UNKNOWN) { + totalUncompressedSize += size + } else { + inaccurateTotalUncompressedSize = true + } + } + + fun totalUncompressedSize(): Long? = + if (totalUncompressedSize != ArchiveEntry.SIZE_UNKNOWN) totalUncompressedSize else null + + override fun toString(): String = fileName + + open fun relativePath(): Path? = archiveEntry?.name?.let { Paths.get(it) } + + open fun absolutePath(archiveFilePath: Path): Path? { + if (archiveEntry == null) { + return null + } + + return archiveFilePath.parent + .resolve("${archiveFilePath.fileName}!") + .resolve(Paths.get(archiveEntry!!.name)) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ArchiveNode) return false + + if (relativePath != other.relativePath) return false + + return true + } + + override fun hashCode(): Int = relativePath.hashCode() + + companion object { + + fun toRelativePath(path: String) = + if (path.startsWith("/")) path.substringAfter("/") else path + + fun findInLastPathComponent(treePath: TreePath): ArchiveNode? = + treePath.lastPathComponent + ?.safeCastTo() + ?.userObject + ?.safeCastTo() + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class FileNode(fileName: String, archiveEntry: ArchiveEntry) : + ArchiveNode( + fileName, + determineIcon(fileName), + archiveEntry.size, + toRelativePath(archiveEntry.name), + archiveEntry, + ) { + + override fun relativePath(): Path = super.relativePath()!! + + override fun absolutePath(archiveFilePath: Path): Path = super.absolutePath(archiveFilePath)!! + + fun isTextFile() = + textFileNames.contains(fileName) || + textFileExtensions.contains( + fileName.substringAfterLast(".").toLowerCasePreservingASCIIRules() + ) + + companion object { + + private fun determineIcon(fileName: String): Icon { + when (fileName) { + ".htaccess" -> return AllIcons.FileTypes.Htaccess + "config", + "init", + ".gitignore", + ".gitattributes", + ".editorconfig", + "Dockerfile", + "index", + "Makefile" -> return AllIcons.FileTypes.Config + "README", + "LICENSE", + "NOTICE", + "CHANGELOG" -> AllIcons.FileTypes.Text + } + + return when (fileName.substringAfterLast(".").toLowerCasePreservingASCIIRules()) { + "mf" -> AllIcons.FileTypes.Manifest + "zip", + "tar", + "jar" -> AllIcons.FileTypes.Archive + "as" -> AllIcons.FileTypes.AS + "aj" -> AllIcons.FileTypes.Aspectj + "config", + "conf", + "cfg", + "ini" -> AllIcons.FileTypes.Config + "css", + "scss", + "sass" -> AllIcons.FileTypes.Css + "dtd" -> AllIcons.FileTypes.Dtd + "hprof" -> AllIcons.FileTypes.Hprof + "html", + "htm" -> AllIcons.FileTypes.Html + "xhtml" -> AllIcons.FileTypes.Xhtml + "xml", + "pom" -> AllIcons.FileTypes.Xml + "xsd" -> AllIcons.FileTypes.XsdFile + "jpg", + "jpeg", + "png", + "gif", + "bmp", + "tiff", + "svg", + "psd", + "ai", + "eps", + "raw", + "webp", + "pdf", + "ico" -> AllIcons.FileTypes.Image + "java", + "kt", + "kts", + "groovy", + "scala" -> AllIcons.FileTypes.Java + "class", + "jmod" -> AllIcons.FileTypes.JavaClass + "js" -> AllIcons.FileTypes.JavaScript + "jfr" -> AllIcons.FileTypes.Jfr + "json" -> AllIcons.FileTypes.Json + "yaml", + "yml" -> AllIcons.FileTypes.Yaml + "jsp" -> AllIcons.FileTypes.Jsp + "jspx" -> AllIcons.FileTypes.Jspx + "properties" -> AllIcons.FileTypes.Properties + "txt", + "text", + "md", + "log", + "sql", + "csv", + "tex" -> AllIcons.FileTypes.Text + else -> AllIcons.FileTypes.Any_type + } + } + } + + private val textFileNames = + setOf( + "config", + "init", + ".gitignore", + ".gitattributes", + ".editorconfig", + "Dockerfile", + "index", + "Makefile", + "README", + "LICENSE", + "NOTICE", + "CHANGELOG", + ".htaccess", + ) + + private val textFileExtensions = + setOf( + "txt", + "html", + "xml", + "csv", + "json", + "log", + "md", + "css", + "js", + "php", + "py", + "java", + "c", + "cpp", + "rb", + "sql", + "yml", + "yaml", + "ini", + "conf", + "cfg", + "xml", + "htm", + "cs", + "scala", + "groovy", + "sh", + "bat", + "ps1", + "awk", + "sed", + "tex", + "r", + "scss", + "less", + "styl", + "asp", + "jsp", + "aspx", + "xhtml", + "cfc", + "cfm", + "tpl", + "twig", + "handlebars", + "mustache", + "jsx", + "tsx", + "kt", + "kts", + "mf", + "config", + "properties", + "pom", + "text", + "sass", + ) + } + + // -- Inner Type ---------------------------------------------------------- // + + private open class DirectoryNode( + name: String, + relativePath: String, + archiveEntry: ArchiveEntry?, + icon: Icon = AllIcons.Nodes.Folder, + ) : ArchiveNode(name, icon, 0, toRelativePath(relativePath), archiveEntry) { + + var totalChildren: Int = 0 + val children: MutableList = mutableListOf() + } + + // -- Inner Type ---------------------------------------------------------- // + + private class RootNode(val archiveFilePath: Path) : + DirectoryNode(archiveFilePath.fileName.toString(), "", null, AllIcons.FileTypes.Archive) { + + override fun relativePath(): Path? = null + + override fun absolutePath(archiveFilePath: Path): Path = archiveFilePath + + fun iterateEntries(visitor: (ArchiveEntry, () -> InputStream) -> Boolean) { + if (archiveFilePath.fileName.extension() == "7z") { + val sevenZFile = SevenZFile.Builder().setFile(archiveFilePath.toFile()).get() + var archiveEntry = sevenZFile.nextEntry + while (archiveEntry != null) { + if (!visitor(archiveEntry) { sevenZFile.getInputStream(archiveEntry) }) { + break + } + archiveEntry = sevenZFile.nextEntry + } + } else { + var archiveFileInputStream = BufferedInputStream(FileInputStream(archiveFilePath.toFile())) + if (GzipUtils.isCompressedFileName(archiveFilePath.fileName.toString())) { + archiveFileInputStream = + BufferedInputStream(GzipCompressorInputStream(archiveFileInputStream)) + } + + val archiveInputStream: ArchiveInputStream<*> = + ArchiveStreamFactory().createArchiveInputStream(archiveFileInputStream) + archiveInputStream.use { archiveInputStream0 -> + var archiveEntry = archiveInputStream0.nextEntry + while (archiveEntry != null) { + if (!visitor(archiveEntry) { archiveInputStream0 }) { + break + } + archiveEntry = archiveInputStream0.nextEntry + } + } + } + } + + fun readEntry(archiveEntry: ArchiveEntry): ByteArray { + var result: ByteArray? = null + iterateEntries { archiveEntry0, archiveInputStream -> + if (archiveEntry0 == archiveEntry) { + result = IOUtils.toByteArray(archiveInputStream()) + false + } else { + true + } + } + return result + ?: throw IllegalStateException("Unable to find archive entry: ${archiveEntry.name}") + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ChooseArchiveFileToOpenAction( + private val project: Project?, + private val tree: Tree?, + private val lastSelectedOpenedDirectoryPath: ValueProperty, + private val openArchiveCallback: (Path) -> Unit, + ) : DumbAwareAction("Open Archive File", null, AllIcons.Actions.MenuOpen) { + + override fun actionPerformed(e: AnActionEvent) { + openArchiveDialog() + } + + @Suppress("UnstableApiUsage") + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = tree != null && UIUtil.isShowing(tree) + } + + override fun getActionUpdateThread() = ActionUpdateThread.EDT + + private fun openArchiveDialog() { + ApplicationManager.getApplication().executeOnPooledThread { + val startPath = + VirtualFileManager.getInstance().findFileByUrl(lastSelectedOpenedDirectoryPath.get()) + + ApplicationManager.getApplication().invokeLater { + val descriptor = + FileChooserDescriptorFactory.createSingleFileDescriptor() + .withTitle("Open Archive File") + .withExtensionFilter("Archive files", *supportedArchiveExtensions) + val fileToOpen = FileChooser.chooseFile(descriptor, project, startPath) + if (fileToOpen != null) { + openArchiveCallback(fileToOpen.toNioPath()) + } + } + } + } + + @Suppress("DialogTitleCapitalization") + fun toHyperlinkLabel() = + HyperlinkLabel(templatePresentation.text).apply { + icon = templatePresentation.icon + + addHyperlinkListener { openArchiveDialog() } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + class OpenArchiveFileInUnarchiverAction : + DumbAwareAction("Unarchiver", "Open archive file in the developer tool 'Unarchiver'.", null) { + + private val supportedArchiveExtensions = Companion.supportedArchiveExtensions.toSet() + + override fun update(e: AnActionEvent) { + val selectedFiles = CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(e.dataContext) + e.presentation.isVisible = isSelectedFileSupported(selectedFiles) + } + + override fun actionPerformed(e: AnActionEvent) { + val selectedFiles = CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(e.dataContext) + if (!isSelectedFileSupported(selectedFiles)) { + return + } + + var sanitizedFilePath = selectedFiles!![0].path + if ( + selectedFiles[0].fileSystem.protocol == StandardFileSystems.JAR_PROTOCOL && + sanitizedFilePath.endsWith(URLUtil.JAR_SEPARATOR) + ) { + sanitizedFilePath = sanitizedFilePath.substringBeforeLast(URLUtil.JAR_SEPARATOR) + } + + e.project + ?.service() + ?.openTool(OpenUnarchiverContext(Paths.get(sanitizedFilePath)), openUnarchiverReference) + } + + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + private fun isSelectedFileSupported(selectedFiles: Array?) = + selectedFiles?.size == 1 && + selectedFiles.any { + it.extension != null && supportedArchiveExtensions.contains(it.extension) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class SortingMode(val title: String, val comparator: Comparator) { + + UNCOMPRESSED_SIZE_ASC( + "Uncompressed size (ascending)", + { a, b -> compareValues(a.totalUncompressedSize(), b.totalUncompressedSize()) }, + ), + UNCOMPRESSED_SIZE_DESC( + "Uncompressed size (descending)", + { a, b -> compareValues(b.totalUncompressedSize(), a.totalUncompressedSize()) }, + ), + FILENAME_ASC("Filename (ascending)", { a, b -> a.fileName.compareTo(b.fileName) }), + FILENAME_DESC("Filename (descending)", { a, b -> b.fileName.compareTo(a.fileName) }), + } + + // -- Inner Type ---------------------------------------------------------- // + + private class ArchiveTreeNodeRenderer( + val showArchiveNodeUncompressedSize: ValueProperty, + val showArchiveNodeTotalNumberOfChildren: ValueProperty, + ) : NodeRenderer() { + + override fun customizeCellRenderer( + tree: JTree, + value: Any, + selected: Boolean, + expanded: Boolean, + leaf: Boolean, + row: Int, + hasFocus: Boolean, + ) { + val archiveNode = + value + .uncheckedCastTo(DefaultMutableTreeNode::class) + .userObject + .uncheckedCastTo(ArchiveNode::class) + + icon = archiveNode.icon + append(archiveNode.fileName, SimpleTextAttributes.REGULAR_ATTRIBUTES) + + val metaInformation = mutableListOf() + if (showArchiveNodeUncompressedSize.get()) { + archiveNode.totalUncompressedSize()?.let { + if (!(it == 0L && archiveNode.inaccurateTotalUncompressedSize)) { + metaInformation.add( + "${if (archiveNode.inaccurateTotalUncompressedSize) "~" else ""}${StringUtil.formatFileSize(it)}" + ) + } + } + } + if (showArchiveNodeTotalNumberOfChildren.get() && archiveNode is DirectoryNode) { + metaInformation.add("${archiveNode.totalChildren} entries") + } + if (metaInformation.isNotEmpty()) { + append(" ", SimpleTextAttributes.REGULAR_ATTRIBUTES) + append(metaInformation.joinToString(", "), SimpleTextAttributes.GRAY_SMALL_ATTRIBUTES) + } + } + } + + // -- Inner Type ---------------------------------------------------------- // + + private data class ExtractionContext( + val rootNode: RootNode, + val archiveNodes: List, + val targetDirectoryPath: Path, + val clearTargetDirectory: Boolean, + val createParentDirectories: Boolean, + val preserveDirectoryStructure: Boolean, + val preserveFileAttributes: Boolean, + val openTargetDirectoryAfterExtraction: Boolean, + ) + + // -- Inner Type ---------------------------------------------------------- // + + private data class ArchiveNodesToExtract( + val archiveNodes: List, + val displayPathsWithUncompressedSize: Map, + val totalUncompressedSize: Long, + val inaccurateTotalUncompressedSize: Boolean, + ) + + // -- Inner Type ---------------------------------------------------------- // + + private class ExtractTask(project: Project?, private val extractionContext: ExtractionContext) : + Task.ConditionalModal( + project, + "Extracting ${extractionContext.rootNode.fileName}", + true, + DEAF, + ) { + + override fun run(progressIndicator: ProgressIndicator) { + progressIndicator.checkCanceled() + progressIndicator.text = "Preparing directory structure..." + + if (!Files.exists(extractionContext.targetDirectoryPath)) { + Files.createDirectories(extractionContext.targetDirectoryPath) + } else { + if (!Files.isDirectory(extractionContext.targetDirectoryPath)) { + throw IllegalArgumentException( + "The target path '${extractionContext.targetDirectoryPath}' already exists but it is not a directory" + ) + } else if (extractionContext.clearTargetDirectory) { + FileUtils.cleanDirectory(extractionContext.targetDirectoryPath.toFile()) + } + } + + // Prepare directories... + val preparedFileNodeExtractions = mutableMapOf() + extractionContext.archiveNodes.forEach { archiveNode -> + progressIndicator.checkCanceled() + progressIndicator.text2 = archiveNode.relativePath + + when (archiveNode) { + is DirectoryNode -> + preparedFileNodeExtractions.putAll( + prepareDirectoryNode( + archiveNode, + extractionContext.targetDirectoryPath, + extractionContext, + ) + ) + + is FileNode -> + prepareStandaloneFileNodeExtraction(archiveNode, extractionContext).let { + (archiveEntry, path) -> + preparedFileNodeExtractions[archiveEntry.name] = path + } + + else -> + throw IllegalStateException( + "Unknown archive node type: ${archiveNode::class.qualifiedName}" + ) + } + } + + // Copy files... + val numOfFileNodesToExtract = preparedFileNodeExtractions.size + val archiveEntryToTargetFilePathToCopy = preparedFileNodeExtractions.toMutableMap() + extractionContext.rootNode.iterateEntries { archiveEntry, archiveInputStream -> + if (archiveEntryToTargetFilePathToCopy.containsKey(archiveEntry.name)) { + progressIndicator.checkCanceled() + progressIndicator.text = + "Extracting $numOfFileNodesToExtract file${if (numOfFileNodesToExtract == 1) "s" else ""} (${archiveEntryToTargetFilePathToCopy.size} remaining)..." + progressIndicator.text2 = archiveEntry.name + progressIndicator.fraction = + (numOfFileNodesToExtract - archiveEntryToTargetFilePathToCopy.size.toDouble()) / + numOfFileNodesToExtract + + val targetPath = archiveEntryToTargetFilePathToCopy[archiveEntry.name]!! + Files.newOutputStream(targetPath, WRITE, CREATE_NEW, TRUNCATE_EXISTING).use { outputStream + -> + IOUtils.copy(archiveInputStream(), outputStream) + } + archiveEntryToTargetFilePathToCopy.remove(archiveEntry.name) + if (extractionContext.preserveFileAttributes) { + restoreFileAttributes(archiveEntry, targetPath) + } + } + archiveEntryToTargetFilePathToCopy.isNotEmpty() + } + if (archiveEntryToTargetFilePathToCopy.isNotEmpty()) { + throw IllegalStateException( + "Unable to find archive entries: ${archiveEntryToTargetFilePathToCopy.map { it.key }.joinToString(", ")}" + ) + } + } + + override fun onThrowable(error: Throwable) { + log.warn("Extraction failed", error) + ApplicationManager.getApplication().invokeLater { + Messages.showErrorDialog(project, error.message, "Extraction Failed") + } + } + + override fun onSuccess() { + if (extractionContext.openTargetDirectoryAfterExtraction) { + BrowserUtil.browse(extractionContext.targetDirectoryPath) + } else { + notifyAboutExtractionResult(extractionContext) + } + } + + private fun prepareDirectoryNode( + directoryNode: DirectoryNode, + baseDirectoryPath: Path, + extractionContext: ExtractionContext, + ): Map { + val directoryPath = + if (extractionContext.preserveDirectoryStructure) { + val relativeDirectoryPath = + if (extractionContext.createParentDirectories) { + Paths.get(directoryNode.relativePath) + } else { + Path.of(directoryNode.fileName) + } + val directoryPath = + Files.createDirectories(baseDirectoryPath.resolve(relativeDirectoryPath)) + if (extractionContext.preserveFileAttributes && directoryNode.archiveEntry != null) { + restoreFileAttributes(directoryNode.archiveEntry!!, directoryPath) + } + directoryPath + } else { + baseDirectoryPath + } + + val preparedFileNodeExtractions = mutableMapOf() + directoryNode.children.forEach { archiveNode -> + when (archiveNode) { + is DirectoryNode -> + preparedFileNodeExtractions.putAll( + prepareDirectoryNode(archiveNode, directoryPath, extractionContext) + ) + + is FileNode -> { + val filePath = directoryPath.resolve(archiveNode.fileName) + preparedFileNodeExtractions[archiveNode.archiveEntry!!.name] = filePath + } + + else -> + throw IllegalStateException( + "Unknown archive node type: ${archiveNode::class.qualifiedName}" + ) + } + } + return preparedFileNodeExtractions + } + + /** + * A [FileNode] is "standalone" if its parent [DirectoryNode] is not extracted. This method + * creates the associated directory structure mmit. + */ + private fun prepareStandaloneFileNodeExtraction( + fileNode: FileNode, + extractionContext: ExtractionContext, + ): Pair { + val filePath = + if ( + extractionContext.preserveDirectoryStructure && extractionContext.createParentDirectories + ) { + val targetPath = + extractionContext.targetDirectoryPath.resolve(Paths.get(fileNode.relativePath)) + Files.createDirectories(targetPath.parent) + targetPath + } else { + extractionContext.targetDirectoryPath.resolve(Paths.get(fileNode.fileName)) + } + + return fileNode.archiveEntry!! to filePath + } + + private fun notifyAboutExtractionResult(extractionContext: ExtractionContext) { + val numOfExtractedEntries = extractionContext.archiveNodes.size + NotificationUtils.notifyOnToolWindow( + message = + "$numOfExtractedEntries ${if (numOfExtractedEntries == 1) "entry" else "entries"} have been extracted to: ${extractionContext.targetDirectoryPath}", + project = project, + notificationType = NotificationType.INFORMATION, + object : DumbAwareAction("Open Target Directory") { + + override fun actionPerformed(e: AnActionEvent) { + BrowserUtil.browse(extractionContext.targetDirectoryPath) + } + }, + ) + } + } + + // -- Inner Type ---------------------------------------------------------- // + + data class OpenUnarchiverContext(val archiveFilePath: Path) : OpenDeveloperToolContext + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Unarchiver", contentTitle = CONTENT_TITLE) + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> Unarchiver) = { + assert(ID == context.id) + Unarchiver(project, it, parentDisposable) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val ID = "unarchiver" + private const val CONTENT_TITLE = "Unarchiver" + + private val DEFAULT_SORTING_MODE = SortingMode.FILENAME_ASC + private const val DEFAULT_SHOW_ARCHIVE_NODE_SIZE = true + private const val DEFAULT_SHOW_ARCHIVE_NODE_TOTAL_NUMBER_OF_CHILDREN = true + private const val DEFAULT_OPEN_IN_EDITOR_ON_DOUBLE_CLICK = true + private val DEFAULT_LAST_OPENED_DIRECTORY_PATH = SystemProperties.getUserHome() + private val DEFAULT_LAST_SELECTED_TARGET_DIRECTORY_PATH = SystemProperties.getUserHome() + private const val DEFAULT_CREATE_ARCHIVE_FILENAME_SUB_DIRECTORY = true + private const val DEFAULT_CLEAR_TARGET_DIRECTORY = false + private const val DEFAULT_PRESERVE_DIRECTORY_STRUCTURE = true + private const val DEFAULT_CREATE_PARENT_DIRECTORIES = true + private const val DEFAULT_PRESERVE_FILE_ATTRIBUTES = true + private const val DEFAULT_OPEN_TARGET_DIRECTORY_AFTER_EXTRACTION = true + + private val log = logger() + + val openUnarchiverReference = OpenDeveloperToolReference.of(ID, OpenUnarchiverContext::class) + + private val supportedArchiveExtensions: Array = + // Keep in sync with `org.apache.commons.compress.compressors.gzip.GzipUtils` + setOf("tgz", "taz", "svgz", "cpgz", "wmz", "emz", "gz", "z") + .plus( + ArchiveStreamFactory.findAvailableArchiveInputStreamProviders().map { it.key.lowercase() } + ) + .sorted() + .toTypedArray() + + private fun restoreFileAttributes(archiveEntry: ArchiveEntry, path: Path) { + val attributes = Files.getFileAttributeView(path, BasicFileAttributeView::class.java) + val oldAttributes = attributes.readAttributes() + + val fileTimes = FileTimes.fromArchiveEntry(archiveEntry) + attributes.setTimes( + fileTimes.lastModifiedTime ?: oldAttributes.lastModifiedTime(), + fileTimes.lastAccessTime ?: oldAttributes.lastAccessTime(), + fileTimes.creationTime ?: oldAttributes.creationTime(), + ) + } + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/CodeStyleFormatting.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/CodeStyleFormatting.kt similarity index 51% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/CodeStyleFormatting.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/CodeStyleFormatting.kt index 49d6ec2e..bcfb7292 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/CodeStyleFormatting.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/CodeStyleFormatting.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer import com.intellij.codeInsight.actions.RearrangeCodeProcessor import com.intellij.codeInsight.actions.ReformatCodeProcessor @@ -10,47 +10,52 @@ import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider import com.intellij.testFramework.LightVirtualFile import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.whenItemSelectedFromUi -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation class CodeStyleFormatting( - private val codeStyles: List, + val codeStyles: List, project: Project, context: DeveloperUiToolContext, configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : TextTransformer( - textTransformerContext = TextTransformerContext( - transformActionTitle = "Format", - sourceTitle = "Original", - resultTitle = "Formatted", - diffSupport = DiffSupport( - title = "Code Style Formatting" - ) - ), - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var selectedCodeStyleLanguageId = configuration.register("languageId", FAVORITE_DEFAULT_LANGUAGE_ID) - - // -- Initialization ---------------------------------------------------------------------------------------------- // + parentDisposable: Disposable, +) : + TextTransformer( + textTransformerContext = + TextTransformerContext( + transformActionTitle = "Format", + sourceTitle = "Original", + resultTitle = "Formatted", + diffSupport = DiffSupport(title = "Code Style Formatting"), + ), + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + ) { + // -- Properties ---------------------------------------------------------- // + + private var selectedCodeStyleLanguageId = + configuration.register("languageId", FAVORITE_DEFAULT_LANGUAGE_ID) + + // -- Initialization ------------------------------------------------------ // init { check(codeStyles.isNotEmpty()) // Validate if selected language is still available if (codeStyles.find { it.language.id == selectedCodeStyleLanguageId.get() } == null) { - selectedCodeStyleLanguageId.set((codeStyles.find { it.language.id == FAVORITE_DEFAULT_LANGUAGE_ID } ?: codeStyles.first()).language.id) + selectedCodeStyleLanguageId.set( + (codeStyles.find { it.language.id == FAVORITE_DEFAULT_LANGUAGE_ID } ?: codeStyles.first()) + .language + .id + ) } } - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Exposed Methods ----------------------------------------------------- // @Suppress("UnstableApiUsage") override fun Panel.buildTopConfigurationUi() { @@ -59,9 +64,7 @@ class CodeStyleFormatting( row { comboBox(codeStyles.toList()) .label("Language:") - .applyToComponent { - selectedItem = selectedCodeStyle - } + .applyToComponent { selectedItem = selectedCodeStyle } .whenItemSelectedFromUi { selectedCodeStyleLanguageId.set(it.language.id) setLanguage(it.language) @@ -72,49 +75,55 @@ class CodeStyleFormatting( } override fun transform() { - val workingVirtualFile = LightVirtualFile(this.javaClass.canonicalName, getSelectedCodeStyle().language, sourceText.get()) + val workingVirtualFile = + LightVirtualFile( + this.javaClass.canonicalName, + getSelectedCodeStyle().language, + sourceText.get(), + ) PsiManager.getInstance(project!!).findFile(workingVirtualFile)?.let { workingPsiFile -> - val processor = RearrangeCodeProcessor(ReformatCodeProcessor(project, workingPsiFile, null, false)) - processor.setPostRunnable { - resultText.set(workingPsiFile.text) - } + val processor = + RearrangeCodeProcessor(ReformatCodeProcessor(project, workingPsiFile, null, false)) + processor.setPostRunnable { resultText.set(workingPsiFile.text) } processor.run() } ?: error("snh: Can't get PSI file for `LightVirtualFile`") } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // - private fun getSelectedCodeStyle() = codeStyles.first { it.language.id == selectedCodeStyleLanguageId.get() } + private fun getSelectedCodeStyle() = + codeStyles.first { it.language.id == selectedCodeStyleLanguageId.get() } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // data class CodeStyle(val title: String, val language: Language) { override fun toString(): String = title } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Code Style Formatting", - contentTitle = "Code Style Formatting" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation( + menuTitle = "Code Style Formatting", + contentTitle = "Code Style Formatting", + ) override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> CodeStyleFormatting)? { if (project == null) { return null } - val codeStyles: List = LanguageCodeStyleSettingsProvider - .EP_NAME.extensionList - .sortedBy { it.language.displayName } - .map { CodeStyle(it.language.displayName, it.language) } + val codeStyles: List = + LanguageCodeStyleSettingsProvider.EP_NAME.extensionList + .sortedBy { it.language.displayName } + .map { CodeStyle(it.language.displayName, it.language) } if (codeStyles.isEmpty()) { return null } @@ -125,10 +134,10 @@ class CodeStyleFormatting( } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { private const val FAVORITE_DEFAULT_LANGUAGE_ID = "JSON" } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/HashingTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/HashingTransformer.kt new file mode 100644 index 00000000..ecb2a407 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/HashingTransformer.kt @@ -0,0 +1,98 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import dev.turingcomplete.intellijdevelopertoolsplugin.common.toMessageDigest +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.ConversionSideHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.TextInputOutputHandler.BytesToTextMode.BYTES_TO_HEX +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.UndirectionalConverter +import java.security.Security + +class HashingTransformer( + context: DeveloperUiToolContext, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + project: Project?, +) : + UndirectionalConverter( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + title = "Hashing", + sourceTitle = "Plain", + targetTitle = "Hashed", + toTargetTitle = "Hash", + ) { + // -- Properties ---------------------------------------------------------- // + + private var selectedAlgorithm = configuration.register("algorithm", DEFAULT_ALGORITHM) + + // -- Initialization ------------------------------------------------------ // + + init { + check(messageDigestAlgorithms.isNotEmpty()) + + // Validate if selected algorithm is still available + if (messageDigestAlgorithms.find { it == selectedAlgorithm.get() } == null) { + selectedAlgorithm.set( + messageDigestAlgorithms.find { it == DEFAULT_ALGORITHM } ?: messageDigestAlgorithms.first() + ) + } + } + + // -- Exposed Methods ----------------------------------------------------- // + + override fun ConversionSideHandler.addTargetTextInputOutputHandler() { + addTextInputOutputHandler( + id = defaultTargetInputOutputHandlerId, + bytesToTextMode = BYTES_TO_HEX, + ) + } + + override fun doConvertToTarget(source: ByteArray): ByteArray = + selectedAlgorithm.get().toMessageDigest().digest(source) + + override fun Panel.buildSourceTopConfigurationUi() { + row { comboBox(messageDigestAlgorithms).label("Algorithm:").bindItem(selectedAlgorithm) } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Hashing", contentTitle = "Hashing Transformer") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> HashingTransformer)? { + if (messageDigestAlgorithms.isEmpty()) { + return null + } + + return { configuration -> + HashingTransformer(context, configuration, parentDisposable, project) + } + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val DEFAULT_ALGORITHM = "SHA-256" + private val messageDigestAlgorithms: List by lazy { + Security.getAlgorithms("MessageDigest").sorted() + } + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/HmacTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/HmacTransformer.kt new file mode 100644 index 00000000..fdf7c31c --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/HmacTransformer.kt @@ -0,0 +1,210 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer + +import com.intellij.icons.AllIcons +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.actionButton +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.whenItemSelectedFromUi +import dev.turingcomplete.intellijdevelopertoolsplugin.common.decodeBase64String +import dev.turingcomplete.intellijdevelopertoolsplugin.common.emptyByteArray +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.SENSITIVE +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings.Companion.generalSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.GeneralSettings.Companion.createSensitiveInputsHandlingToolTipText +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.SimpleToggleAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.UiUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.registerDynamicToolTip +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateNonEmpty +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.ConversionSideHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.TextInputOutputHandler.BytesToTextMode.BYTES_TO_HEX +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.UndirectionalConverter +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.HmacTransformer.SecretKeyEncodingMode.BASE32 +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.HmacTransformer.SecretKeyEncodingMode.BASE64 +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.HmacTransformer.SecretKeyEncodingMode.RAW +import java.security.Security +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import org.apache.commons.codec.binary.Base32 + +class HmacTransformer( + context: DeveloperUiToolContext, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + project: Project?, +) : + UndirectionalConverter( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + title = "HMAC", + sourceTitle = "Data", + targetTitle = "Hash", + toTargetTitle = "Generate", + ) { + // -- Properties ---------------------------------------------------------- // + + private var selectedAlgorithm = configuration.register("algorithm", DEFAULT_ALGORITHM) + + private val secretKey = + configuration.register("secretKey", SECRET_KEY_DEFAULT, SENSITIVE, EXAMPLE_SECRET) + private val secretKeyEncodingMode = + configuration.register("secretKeyEncodingMode", RAW, CONFIGURATION) + + // -- Initialization ------------------------------------------------------ // + + init { + check(hmacAlgorithms.isNotEmpty()) + + // Validate if selected algorithm is still available + val selectedAlgorithm = selectedAlgorithm.get() + if (hmacAlgorithms.find { it.algorithm == selectedAlgorithm } == null) { + this.selectedAlgorithm.set( + (hmacAlgorithms.find { it.algorithm.equals(DEFAULT_ALGORITHM, true) } + ?: hmacAlgorithms.first()) + .algorithm + ) + } + } + + // -- Exposed Methods ----------------------------------------------------- // + + override fun ConversionSideHandler.addTargetTextInputOutputHandler() { + addTextInputOutputHandler( + id = defaultTargetInputOutputHandlerId, + bytesToTextMode = BYTES_TO_HEX, + ) + } + + override fun doConvertToTarget(source: ByteArray): ByteArray { + val secretKeyValue = secretKey.get() + if (secretKeyValue.isEmpty()) { + return emptyByteArray + } + + return Mac.getInstance(selectedAlgorithm.get()).run { + val secretKey = + when (secretKeyEncodingMode.get()) { + RAW -> secretKeyValue + BASE32 -> Base32().decode(secretKeyValue).decodeToString() + BASE64 -> secretKeyValue.decodeBase64String() + } + init(SecretKeySpec(secretKey.encodeToByteArray(), selectedAlgorithm.get())) + doFinal(source) + } + } + + override fun Panel.buildSourceTopConfigurationUi() { + row { + comboBox(hmacAlgorithms) + .label("Algorithm:") + .applyToComponent { + selectedItem = hmacAlgorithms.find { it.algorithm == selectedAlgorithm.get() } + } + .whenItemSelectedFromUi { selectedAlgorithm.set(it.algorithm) } + } + } + + override fun Panel.buildSourceBottomConfigurationUi() { + row { + expandableTextField() + .label("Secret key:") + .align(AlignX.FILL) + .bindText(secretKey) + .validateNonEmpty("A secret key must be provided") + .gap(RightGap.SMALL) + .resizableColumn() + .registerDynamicToolTip { generalSettings.createSensitiveInputsHandlingToolTipText() } + + val encodingActions = + mutableListOf().apply { + SecretKeyEncodingMode.entries.forEach { secretKeyEncodingModeValue -> + add( + SimpleToggleAction( + text = secretKeyEncodingModeValue.title, + icon = AllIcons.Actions.ToggleSoftWrap, + isSelected = { secretKeyEncodingMode.get() == secretKeyEncodingModeValue }, + setSelected = { secretKeyEncodingMode.set(secretKeyEncodingModeValue) }, + ) + ) + } + } + actionButton( + UiUtils.actionsPopup( + title = "Encoding", + icon = AllIcons.General.Settings, + actions = encodingActions, + ) + ) + } + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + data class HmacAlgorithm(val title: String, val algorithm: String) { + + override fun toString(): String = title + } + + // -- Inner Type ---------------------------------------------------------- // + + enum class SecretKeyEncodingMode(val title: String) { + + RAW("Raw"), + BASE32("Base32 Encoded"), + BASE64("Base64 Encoded"), + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "HMAC", contentTitle = "HMAC Transformer") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> HmacTransformer)? { + if (hmacAlgorithms.isEmpty()) { + return null + } + + return { configuration -> HmacTransformer(context, configuration, parentDisposable, project) } + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val DEFAULT_ALGORITHM = "HMACSHA256" + private const val SECRET_KEY_DEFAULT = "" + + val hmacAlgorithms: List by lazy { + Security.getAlgorithms("Mac") + .asSequence() + .filter { it.startsWith("HMAC") } + .filter { + // Would require a complex PBEKey + !it.contains("PBE") + } + .map { HmacAlgorithm(it.replace("HMAC", "Hmac"), it) } + .sortedBy { it.title } + .toList() + } + + private const val EXAMPLE_SECRET = "s3cre!" + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/JsonPathTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/JsonPathTransformer.kt similarity index 74% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/JsonPathTransformer.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/JsonPathTransformer.kt index 90689478..30956d24 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/JsonPathTransformer.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/JsonPathTransformer.kt @@ -1,6 +1,4 @@ -@file:Suppress("UnstableApiUsage") - -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer import com.fasterxml.jackson.databind.node.ArrayNode import com.intellij.codeInsight.actions.RearrangeCodeProcessor @@ -37,44 +35,47 @@ import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.JsonPathException import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.copyable -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.objectMapper -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.CONFIGURATION +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.ObjectMapperService +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.copyable import javax.swing.JComponent +import org.intellij.lang.annotations.Language class JsonPathTransformer( context: DeveloperUiToolContext, configuration: DeveloperToolConfiguration, parentDisposable: Disposable, - project: Project? -) : TextTransformer( - textTransformerContext = TextTransformerContext( - transformActionTitle = "Execute Query", - sourceTitle = "Original", - resultTitle = "Result", - initialSourceExampleText = EXAMPLE_SOURCE, - inputInitialLanguage = JsonLanguage.INSTANCE, - outputInitialLanguage = JsonLanguage.INSTANCE - ), - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // + project: Project?, +) : + TextTransformer( + textTransformerContext = + TextTransformerContext( + transformActionTitle = "Execute Query", + sourceTitle = "Original", + resultTitle = "Result", + initialSourceExampleText = EXAMPLE_SOURCE, + inputInitialLanguage = JsonLanguage.INSTANCE, + outputInitialLanguage = JsonLanguage.INSTANCE, + ), + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + ) { + // -- Properties ---------------------------------------------------------- // private val queryText = configuration.register("contentText", "", INPUT, EXAMPLE_QUERY) private val formatResult = configuration.register("formatResult", true, CONFIGURATION) private var errorHolder = ErrorHolder() - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // override fun Panel.buildMiddleConfigurationUi() { row { @@ -93,9 +94,9 @@ class JsonPathTransformer( } override fun Row.buildAdditionalActionsUi() { - checkBox("Format result") - .bindSelected(formatResult) - .whenStateChangedFromUi { configurationChanged(queryText) } + checkBox("Format result").bindSelected(formatResult).whenStateChangedFromUi { + configurationChanged(queryText) + } } override fun transform() { @@ -107,21 +108,25 @@ class JsonPathTransformer( } try { - val resultJsonText = when (val resultJsonNode = JsonPath.parse(sourceText.get(), jsonPathConfiguration).read(query)) { - is ArrayNode -> objectMapper.writeValueAsString(resultJsonNode) - else -> resultJsonNode.toString() - } + val resultJsonText = + when ( + val resultJsonNode = + JsonPath.parse(sourceText.get(), jsonPathConfiguration).read(query) + ) { + is ArrayNode -> + ObjectMapperService.instance.jsonMapper().writeValueAsString(resultJsonNode) + else -> resultJsonNode.toString() + } if (formatResult.get()) { - val workingVirtualFile = LightVirtualFile(this.javaClass.canonicalName, JsonLanguage.INSTANCE, resultJsonText) + val workingVirtualFile = + LightVirtualFile(this.javaClass.canonicalName, JsonLanguage.INSTANCE, resultJsonText) PsiManager.getInstance(project!!).findFile(workingVirtualFile)?.let { workingPsiFile -> - val processor = RearrangeCodeProcessor(ReformatCodeProcessor(project, workingPsiFile, null, false)) - processor.setPostRunnable { - resultText.set(workingPsiFile.text) - } + val processor = + RearrangeCodeProcessor(ReformatCodeProcessor(project, workingPsiFile, null, false)) + processor.setPostRunnable { resultText.set(workingPsiFile.text) } processor.run() } ?: error("snh: Can't get PSI file for `LightVirtualFile`") - } - else { + } else { resultText.set(resultJsonText) } } catch (e: JsonPathException) { @@ -133,20 +138,18 @@ class JsonPathTransformer( validate() } - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "JSON Path", - contentTitle = "JSON Path Transformer" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "JSON Path", contentTitle = "JSON Path Transformer") override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> JsonPathTransformer)? { if (project == null) { return null @@ -158,7 +161,7 @@ class JsonPathTransformer( } } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private class ShowOperatorsHelpPopup(val helpButton: () -> JComponent) : DumbAwareAction("Operators Help", null, AllIcons.General.ContextHelp), PopupAction { @@ -181,7 +184,7 @@ class JsonPathTransformer( override fun getActionUpdateThread() = ActionUpdateThread.BGT } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { @@ -192,8 +195,9 @@ class JsonPathTransformer( .build() } - @org.intellij.lang.annotations.Language("JSON") - private const val EXAMPLE_SOURCE = """{ + @Language("JSON") + private const val EXAMPLE_SOURCE = + """{ "starWars": { "characters": [ { @@ -230,8 +234,9 @@ class JsonPathTransformer( private const val EXAMPLE_QUERY = "\$.starWars.characters..forename" - private val operatorsHelpPanel = JBLabel( - """ + private val operatorsHelpPanel = + JBLabel( + """

    Operators

    @@ -247,7 +252,7 @@ class JsonPathTransformer(
    [<start>:<end>:<step>]Selects a range of child elements with the specified step.
    [?(<expression>)]Filter expression. Expression must evaluate to a boolean value.
    - +

    Examples

    @@ -263,7 +268,9 @@ class JsonPathTransformer(
    ExampleDescription
    ${'$'}..movie[-1:].directorSelects the director of the last movie in all sub-objects of the root.
    - """.trimIndent() - ).copyable() + """ + .trimIndent() + ) + .copyable() } -} \ No newline at end of file +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/SqlFormattingTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/SqlFormattingTransformer.kt new file mode 100644 index 00000000..b8b95956 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/SqlFormattingTransformer.kt @@ -0,0 +1,146 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer + +import com.github.vertical_blank.sqlformatter.SqlFormatter +import com.github.vertical_blank.sqlformatter.core.FormatConfig +import com.github.vertical_blank.sqlformatter.languages.Dialect +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindSelected +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bindIntTextImproved +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.validateLongValue +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.ConversionSideHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.UndirectionalConverter + +class SqlFormattingTransformer( + context: DeveloperUiToolContext, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + project: Project?, +) : + UndirectionalConverter( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + title = "SQL Formatting", + sourceTitle = "Plain SQL", + targetTitle = "Formatted SQL", + toTargetTitle = "Format", + ), + DeveloperToolConfiguration.ChangeListener { + // -- Properties ---------------------------------------------------------- // + + private var dialect = configuration.register("dialect", DEFAULT_DIALECT) + private var indentSpaces = configuration.register("indentSpaces", DEFAULT_INDENT_SPACES) + private var uppercase = configuration.register("uppercase", DEFAULT_UPPERCASE) + private var linesBetweenQueries = + configuration.register("linesBetweenQueries", DEFAULT_LINES_BETWEEN_QUERIES) + private var maxColumnLength = configuration.register("maxColumnLength", DEFAULT_MAX_COLUMN_LENGTH) + + private lateinit var formatConfig: FormatConfig + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun ConversionSideHandler.addSourceTextInputOutputHandler() { + addTextInputOutputHandler( + id = defaultSourceInputOutputHandlerId, + exampleText = EXAMPLE_SOURCE_TEXT, + ) + } + + override fun Panel.buildSourceTopConfigurationUi() { + row { comboBox(Dialect.entries).label("Dialect:").bindItem(dialect) } + .layout(RowLayout.PARENT_GRID) + + row { + textField() + .label("Indent spaces:") + .bindIntTextImproved(indentSpaces) + .validateLongValue(LongRange(0, 99)) + } + .layout(RowLayout.PARENT_GRID) + + row { + textField() + .label("Lines between queries:") + .bindIntTextImproved(linesBetweenQueries) + .validateLongValue(LongRange(0, 99)) + } + .layout(RowLayout.PARENT_GRID) + + row { + textField() + .label("Maximum column length:") + .bindIntTextImproved(maxColumnLength) + .validateLongValue(LongRange(0, 99)) + } + .layout(RowLayout.PARENT_GRID) + + row { checkBox("Convert keywords to uppercase").bindSelected(uppercase) } + .layout(RowLayout.PARENT_GRID) + } + + override fun afterBuildUi() { + updateFormatConfig() + super.afterBuildUi() + } + + override fun doConvertToTarget(source: ByteArray): ByteArray = + SqlFormatter.of(dialect.get()).format(String(source), formatConfig).toByteArray() + + override fun configurationChanged(property: ValueProperty) { + updateFormatConfig() + super.configurationChanged(property) + } + + // -- Private Methods ----------------------------------------------------- // + + private fun updateFormatConfig() { + formatConfig = + FormatConfig.builder() + .indent(" ".repeat(indentSpaces.get())) + .uppercase(uppercase.get()) + .linesBetweenQueries(linesBetweenQueries.get()) + .maxColumnLength(maxColumnLength.get()) + .build() + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "SQL Formatting", contentTitle = "SQL Formatting") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> SqlFormattingTransformer) = { configuration -> + SqlFormattingTransformer(context, configuration, parentDisposable, project) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val EXAMPLE_SOURCE_TEXT = + "select c.id, c.name, o.address, o.orderedAt from customers c left join orders o ON (o.customerId = c.id) order by o.orderedAt" + + private val DEFAULT_DIALECT = Dialect.N1ql + private const val DEFAULT_INDENT_SPACES = 2 + private const val DEFAULT_UPPERCASE = true + private const val DEFAULT_LINES_BETWEEN_QUERIES = 1 + private const val DEFAULT_MAX_COLUMN_LENGTH = 30 + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextCaseTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextCaseTransformer.kt new file mode 100644 index 00000000..ea0bd065 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextCaseTransformer.kt @@ -0,0 +1,179 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.selected +import dev.turingcomplete.intellijdevelopertoolsplugin.common.TextCaseUtils +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bind +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.ConversionSideHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.UndirectionalConverter +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.TextCaseTransformer.OriginalParsingMode.AUTOMATIC_DETECTION +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.TextCaseTransformer.OriginalParsingMode.FIXED_TEXT_CASE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.TextCaseTransformer.OriginalParsingMode.INDIVIDUAL_DELIMITER +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.TextCaseTransformer.TextCase.COBOL_CASE +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.TextCaseTransformer.TextCase.STRICT_CAMEL_CASE +import dev.turingcomplete.textcaseconverter.StandardTextCases +import dev.turingcomplete.textcaseconverter.TextCase as StandardTextCase +import dev.turingcomplete.textcaseconverter.toTextCase +import dev.turingcomplete.textcaseconverter.toWordsSplitter + +class TextCaseTransformer( + context: DeveloperUiToolContext, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + project: Project?, +) : + UndirectionalConverter( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + title = "Text Case", + sourceTitle = "Original", + targetTitle = "Target", + toTargetTitle = "Transform", + ) { + // -- Properties ---------------------------------------------------------- // + + private var originalParsingMode = + configuration.register("originalParsingMode", AUTOMATIC_DETECTION) + private var individualDelimiter = configuration.register("individualDelimiter", " ") + private var inputTextCase = configuration.register("inputTextCase", STRICT_CAMEL_CASE) + private var outputTextCase = configuration.register("outputTextCase", COBOL_CASE) + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun ConversionSideHandler.addSourceTextInputOutputHandler() { + addTextInputOutputHandler( + id = defaultSourceInputOutputHandlerId, + exampleText = EXAMPLE_SOURCE_TEXT, + ) + } + + override fun doConvertToTarget(source: ByteArray): ByteArray { + val sourceText = String(source) + val wordsSplitter = getInputWordsSplitter(sourceText) + return sourceText.toTextCase(outputTextCase.get().textCase, wordsSplitter).toByteArray() + } + + override fun Panel.buildSourceBottomConfigurationUi() { + buttonsGroup("Original:") { + row { radioButton("Automatic detection").bind(originalParsingMode, AUTOMATIC_DETECTION) } + + row { + val fixedTextCaseRadioButton = + radioButton("Fixed text case:") + .bind(originalParsingMode, FIXED_TEXT_CASE) + .gap(RightGap.SMALL) + comboBox(TextCase.entries) + .bindItem(inputTextCase) + .enabledIf(fixedTextCaseRadioButton.selected) + .component + } + + row { + val individualDelimiterRadioButton = + radioButton("Split words by:") + .bind(originalParsingMode, INDIVIDUAL_DELIMITER) + .gap(RightGap.SMALL) + textField() + .bindText(individualDelimiter) + .enabledIf(individualDelimiterRadioButton.selected) + .component + } + } + + row { comboBox(TextCase.entries).label("Target:").bindItem(outputTextCase) } + } + + // override fun Panel.buildDebugComponent() { + // group("Input Words") { + // row { + // val inputWords = getInputWordsSplitter().split(sourceText.get()) + // if (inputWords.isEmpty()) { + // label("None") + // } else { + // val wordsList = + // inputWords.joinToString(separator = "", prefix = "
      ", postfix = "
    ") { + // "
  • $it
  • " + // } + // label("$wordsList") + // } + // } + // } + // } + + // -- Private Methods ----------------------------------------------------- // + + private fun getInputWordsSplitter(sourceText: String) = + when (originalParsingMode.get()) { + AUTOMATIC_DETECTION -> + TextCaseUtils.determineWordsSplitter(sourceText, inputTextCase.get().textCase) + FIXED_TEXT_CASE -> inputTextCase.get().textCase.wordsSplitter() + INDIVIDUAL_DELIMITER -> individualDelimiter.get().toWordsSplitter() + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class TextCase(val textCase: StandardTextCase) { + + SCREAMING_SNAKE_CASE(StandardTextCases.SCREAMING_SNAKE_CASE), + SOFT_CAMEL_CASE(StandardTextCases.SOFT_CAMEL_CASE), + STRICT_CAMEL_CASE(StandardTextCases.STRICT_CAMEL_CASE), + PASCAL_CASE(StandardTextCases.PASCAL_CASE), + SNAKE_CASE(StandardTextCases.SNAKE_CASE), + KEBAB_CASE(StandardTextCases.KEBAB_CASE), + TRAIN_CASE(StandardTextCases.TRAIN_CASE), + COBOL_CASE(StandardTextCases.COBOL_CASE), + PASCAL_SNAKE_CASE(StandardTextCases.PASCAL_SNAKE_CASE), + CAMEL_SNAKE_CASE(StandardTextCases.CAMEL_SNAKE_CASE), + LOWER_CASE(StandardTextCases.LOWER_CASE), + UPPER_CASE(StandardTextCases.UPPER_CASE), + INVERTED_CASE(StandardTextCases.INVERTED_CASE), + ALTERNATING_CASE(StandardTextCases.ALTERNATING_CASE), + DOT_CASE(StandardTextCases.DOT_CASE); + + override fun toString(): String = "${textCase.title()} (${textCase.example()})" + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class OriginalParsingMode { + + AUTOMATIC_DETECTION, + FIXED_TEXT_CASE, + INDIVIDUAL_DELIMITER, + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Text Case", contentTitle = "Text Case Transformer") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> TextCaseTransformer) = { configuration -> + TextCaseTransformer(context, configuration, parentDisposable, project) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + const val EXAMPLE_SOURCE_TEXT = "thisIsAnExampleText" + } +} diff --git a/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextFilterTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextFilterTransformer.kt new file mode 100644 index 00000000..617e5840 --- /dev/null +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextFilterTransformer.kt @@ -0,0 +1,239 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.BottomGap +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.selected +import dev.turingcomplete.intellijdevelopertoolsplugin.common.emptyByteArray +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.ErrorHolder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.bind +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex.RegexTextField +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.regex.SelectRegexOptionsAction +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.ConversionSideHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.UndirectionalConverter + +class TextFilterTransformer( + context: DeveloperUiToolContext, + configuration: DeveloperToolConfiguration, + parentDisposable: Disposable, + project: Project?, +) : + UndirectionalConverter( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + title = "Text Filtering", + sourceTitle = "Unfiltered", + targetTitle = "Filtered", + toTargetTitle = "Filter", + ) { + // -- Properties ---------------------------------------------------------- // + + private val tokenMode = configuration.register("tokenSelectionMode", DEFAULT_TOKEN_SELECTION_MODE) + private val filteringMode = configuration.register("filteringMode", DEFAULT_FILTERING_MODE) + private val filteringContainingModeText = + configuration.register( + "filteringContainingModeText", + "", + INPUT, + EXAMPLE_FILTERING_CONTAINING_MODE_TEXT, + ) + private val filteringNotContainingModeText = + configuration.register( + "filteringNotContainingModeText", + "", + INPUT, + EXAMPLE_FILTERING_NOT_CONTAINING_MODE_TEXT, + ) + private val filteringRegexModeText = + configuration.register("filteringRegexModeText", "", INPUT, EXAMPLE_FILTERING_REGEX_MODE_TEXT) + private val filteringRegexModeOptions = configuration.register("filteringRegexModeOptions", 0) + private val filteringRegexModeErrorHolder = ErrorHolder() + + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun ConversionSideHandler.addSourceTextInputOutputHandler() { + addTextInputOutputHandler(id = defaultSourceInputOutputHandlerId, exampleText = EXAMPLE_INPUT) + } + + override fun Panel.buildSourceBottomConfigurationUi() { + row { comboBox(TokenMode.entries).label("Filter:").bindItem(tokenMode) } + .layout(RowLayout.PARENT_GRID) + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + buttonsGroup { + row { + cell() + val containingFilteringModeRadioButton = + radioButton("Containing:") + .bind(filteringMode, FilteringMode.CONTAINING) + .gap(RightGap.SMALL) + expandableTextField() + .bindText(filteringContainingModeText) + .enabledIf(containingFilteringModeRadioButton.selected) + .resizableColumn() + .align(Align.FILL) + } + .layout(RowLayout.PARENT_GRID) + .bottomGap(BottomGap.NONE) + + row { + cell() + val containingFilteringModeRadioButton = + radioButton("Not containing:") + .bind(filteringMode, FilteringMode.NOT_CONTAINING) + .gap(RightGap.SMALL) + expandableTextField() + .bindText(filteringNotContainingModeText) + .enabledIf(containingFilteringModeRadioButton.selected) + .resizableColumn() + .align(Align.FILL) + } + .layout(RowLayout.PARENT_GRID) + .topGap(TopGap.NONE) + .bottomGap(BottomGap.NONE) + + row { + cell() + radioButton("Matching regular expression:") + .bind(filteringMode, FilteringMode.REGEX) + .gap(RightGap.SMALL) + cell(RegexTextField(project, parentDisposable, filteringRegexModeText)) + .validationOnApply(filteringRegexModeErrorHolder.asValidation()) + .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) + .resizableColumn() + .align(Align.FILL) + .gap(RightGap.SMALL) + cell(SelectRegexOptionsAction.createActionButton(filteringRegexModeOptions)) + } + .layout(RowLayout.PARENT_GRID) + .topGap(TopGap.NONE) + } + } + + override fun doConvertToTarget(source: ByteArray): ByteArray { + val tokenFilter: (String) -> Boolean = + when (filteringMode.get()) { + FilteringMode.CONTAINING -> { + val filteringContainingModeTextValue = filteringContainingModeText.get() + ({ it.contains(filteringContainingModeTextValue) }) + } + + FilteringMode.NOT_CONTAINING -> { + val filteringNotContainingModeTextValue = filteringNotContainingModeText.get() + ({ !it.contains(filteringNotContainingModeTextValue) }) + } + + FilteringMode.REGEX -> { + val filteringRegexModeTextValue = + try { + Regex(filteringRegexModeText.get()) + } catch (e: Exception) { + filteringRegexModeErrorHolder.add(e) + return emptyByteArray + } + filteringRegexModeTextValue::matches + } + } + + val sourceText = String(source) + return when (tokenMode.get()) { + TokenMode.WORD -> { + with(StringBuilder()) { + var lastWasWord = false + for (match in WORDS_SPLIT_REGEX.findAll(sourceText)) { + val token = match.value + if (token.isBlank() && !token.contains("\n")) { + if (lastWasWord) { + append(token) + } + } else if (token.contains("\n")) { + append(token) + lastWasWord = false + } else if (tokenFilter(token)) { + append(token) + lastWasWord = true + } else { + lastWasWord = false + } + } + toString().trim() + } + } + + TokenMode.LINE -> sourceText.lines().filter(tokenFilter).joinToString(System.lineSeparator()) + }.encodeToByteArray() + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + + private enum class TokenMode(val pluralTitle: String) { + + WORD("Words"), + LINE("Lines"); + + override fun toString(): String = pluralTitle + } + + // -- Inner Type ---------------------------------------------------------- // + + private enum class FilteringMode { + + CONTAINING, + NOT_CONTAINING, + REGEX, + } + + // -- Inner Type ---------------------------------------------------------- // + + class Factory : DeveloperUiToolFactory { + + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Text Filter", contentTitle = "Text Filter") + + override fun getDeveloperUiToolCreator( + project: Project?, + parentDisposable: Disposable, + context: DeveloperUiToolContext, + ): ((DeveloperToolConfiguration) -> TextFilterTransformer) = { configuration -> + TextFilterTransformer(context, configuration, parentDisposable, project) + } + } + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val WORDS_SPLIT_REGEX = Regex("(\\S+|\\s+)") + + private val DEFAULT_TOKEN_SELECTION_MODE = TokenMode.LINE + private val DEFAULT_FILTERING_MODE = FilteringMode.CONTAINING + + private const val EXAMPLE_FILTERING_CONTAINING_MODE_TEXT = "[error]" + private const val EXAMPLE_FILTERING_NOT_CONTAINING_MODE_TEXT = "[info]" + private const val EXAMPLE_FILTERING_REGEX_MODE_TEXT = "^\\[error\\].*$" + + private val EXAMPLE_INPUT = + """ + [info] Application started + [error] Error occurred while processing request + """ + .trimIndent() + } +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextSortingTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextSortingTransformer.kt similarity index 51% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextSortingTransformer.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextSortingTransformer.kt index 121bf7c7..698b84d0 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextSortingTransformer.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextSortingTransformer.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer import com.intellij.openapi.Disposable import com.intellij.openapi.observable.properties.ObservableMutableProperty @@ -10,41 +10,43 @@ import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.layout.ComboBoxPredicate -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextSortingTransformer.WordsDelimiter.INDIVIDUAL -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextSortingTransformer.WordsDelimiter.LINE_BREAK import dev.turingcomplete.intellijdevelopertoolsplugin.common.makeCaseInsensitive -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -internal class TextSortingTransformer( +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolPresentation +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.ConversionSideHandler +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.UndirectionalConverter +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.TextSortingTransformer.WordsDelimiter.INDIVIDUAL +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.TextSortingTransformer.WordsDelimiter.LINE_BREAK + +class TextSortingTransformer( context: DeveloperUiToolContext, configuration: DeveloperToolConfiguration, parentDisposable: Disposable, - project: Project? -) : TextTransformer( - textTransformerContext = TextTransformerContext( - transformActionTitle = "Sort", + project: Project?, +) : + UndirectionalConverter( + context = context, + configuration = configuration, + parentDisposable = parentDisposable, + project = project, + title = "Text Sorting", sourceTitle = "Unsorted", - resultTitle = "Sorted", - initialSourceExampleText = EXAMPLE_INPUT, - diffSupport = DiffSupport( - title = "Text Sorting" - ) - ), - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // + targetTitle = "Sorted", + toTargetTitle = "Sort", + ) { + // -- Properties ---------------------------------------------------------- // - private var unsortedSplitWordsDelimiter = configuration.register("unsortedPredefinedDelimiter", LINE_BREAK) - private var unsortedIndividualSplitWordsDelimiter = configuration.register("unsortedIndividualSplitWordsDelimiter", " ") + private var unsortedSplitWordsDelimiter = + configuration.register("unsortedPredefinedDelimiter", LINE_BREAK) + private var unsortedIndividualSplitWordsDelimiter = + configuration.register("unsortedIndividualSplitWordsDelimiter", " ") - private var sortedJoinWordsDelimiter = configuration.register("sortedJoinWordsDelimiter", LINE_BREAK) - private var sortedIndividualJoinWordsDelimiter = configuration.register("sortedIndividualJoinWordsDelimiter", " ") + private var sortedJoinWordsDelimiter = + configuration.register("sortedJoinWordsDelimiter", LINE_BREAK) + private var sortedIndividualJoinWordsDelimiter = + configuration.register("sortedIndividualJoinWordsDelimiter", " ") private var sortingOrder = configuration.register("sortingOrder", SortingOrder.LEXICOGRAPHIC) @@ -54,15 +56,23 @@ internal class TextSortingTransformer( private var caseInsensitive = configuration.register("caseInsensitive", false) private var reverseOrder = configuration.register("reverseOrder", false) - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + override fun ConversionSideHandler.addSourceTextInputOutputHandler() { + addTextInputOutputHandler( + id = defaultSourceInputOutputHandlerId, + exampleText = EXAMPLE_SOURCE_TEXT, + ) + } - override fun transform() { - val unsortedSplitWordsDelimiterPattern: Regex = unsortedSplitWordsDelimiter.get().splitPattern - ?: Regex("${Regex.escape(unsortedIndividualSplitWordsDelimiter.get())}+") - val sortedJoinWordsDelimiter = sortedJoinWordsDelimiter.get().joinDelimiter - ?: sortedIndividualJoinWordsDelimiter.get() - var unsortedWords = sourceText.get().split(unsortedSplitWordsDelimiterPattern) + override fun doConvertToTarget(source: ByteArray): ByteArray { + val unsortedSplitWordsDelimiterPattern: Regex = + unsortedSplitWordsDelimiter.get().splitPattern + ?: Regex("${Regex.escape(unsortedIndividualSplitWordsDelimiter.get())}+") + val sortedJoinWordsDelimiter = + sortedJoinWordsDelimiter.get().joinDelimiter ?: sortedIndividualJoinWordsDelimiter.get() + var unsortedWords = String(source).split(unsortedSplitWordsDelimiterPattern) if (trimWords.get()) { unsortedWords = unsortedWords.map { it.trim() } @@ -83,15 +93,15 @@ internal class TextSortingTransformer( } unsortedWords = unsortedWords.sortedWith(comparator) - resultText.set(unsortedWords.joinToString(sortedJoinWordsDelimiter)) + return unsortedWords.joinToString(sortedJoinWordsDelimiter).encodeToByteArray() } - override fun Panel.buildMiddleConfigurationUi() { + override fun Panel.buildSourceBottomConfigurationUi() { row { buildSplitConfigurationUi( "Split unsorted words by:", unsortedSplitWordsDelimiter, - unsortedIndividualSplitWordsDelimiter + unsortedIndividualSplitWordsDelimiter, ) } @@ -99,47 +109,38 @@ internal class TextSortingTransformer( buildSplitConfigurationUi( "Join sorted words by:", sortedJoinWordsDelimiter, - sortedIndividualJoinWordsDelimiter + sortedIndividualJoinWordsDelimiter, ) } row { - comboBox(SortingOrder.entries) - .label("Order:") - .bindItem(sortingOrder) - checkBox("Reverse") - .bindSelected(reverseOrder) - checkBox("Case insensitive") - .bindSelected(caseInsensitive) + comboBox(SortingOrder.entries).label("Order:").bindItem(sortingOrder) + checkBox("Reverse").bindSelected(reverseOrder) + checkBox("Case insensitive").bindSelected(caseInsensitive) } row { - checkBox("Remove duplicates") - .bindSelected(removeDuplicates) - checkBox("Trim words") - .bindSelected(trimWords) - checkBox("Remove blank words") - .bindSelected(removeBlankWords) + checkBox("Remove duplicates").bindSelected(removeDuplicates) + checkBox("Trim words").bindSelected(trimWords) + checkBox("Remove blank words").bindSelected(removeBlankWords) } } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun Row.buildSplitConfigurationUi( title: String, splitWordsDelimiter: ObservableMutableProperty, - individualDelimiter: ObservableMutableProperty + individualDelimiter: ObservableMutableProperty, ) { - val splitWordsDelimiterComboBox = comboBox(WordsDelimiter.entries) - .label(title) - .bindItem(splitWordsDelimiter) - .component + val splitWordsDelimiterComboBox = + comboBox(WordsDelimiter.entries).label(title).bindItem(splitWordsDelimiter).component textField() .bindText(individualDelimiter) .visibleIf(ComboBoxPredicate(splitWordsDelimiterComboBox) { it == INDIVIDUAL }) } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // private enum class SortingOrder(private val title: String, val comparator: Comparator) { @@ -150,9 +151,13 @@ internal class TextSortingTransformer( override fun toString(): String = title } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // - private enum class WordsDelimiter(private val title: String, val splitPattern: Regex?, val joinDelimiter: String?) { + private enum class WordsDelimiter( + private val title: String, + val splitPattern: Regex?, + val joinDelimiter: String?, + ) { LINE_BREAK("Line break", Regex("\\R+"), System.lineSeparator()), SPACE("Whitespace", Regex("\\s+"), " "), @@ -165,28 +170,26 @@ internal class TextSortingTransformer( override fun toString(): String = title } - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // class Factory : DeveloperUiToolFactory { - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Text Sorting", - contentTitle = "Text Sorting" - ) + override fun getDeveloperUiToolPresentation() = + DeveloperUiToolPresentation(menuTitle = "Text Sorting", contentTitle = "Text Sorting") override fun getDeveloperUiToolCreator( project: Project?, parentDisposable: Disposable, - context: DeveloperUiToolContext + context: DeveloperUiToolContext, ): ((DeveloperToolConfiguration) -> TextSortingTransformer) = { configuration -> TextSortingTransformer(context, configuration, parentDisposable, project) } } - // -- Companion Object -------------------------------------------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // companion object { - private const val EXAMPLE_INPUT = "b\nc\na" + private const val EXAMPLE_SOURCE_TEXT = "b\nc\na" } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextTransformer.kt b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextTransformer.kt similarity index 61% rename from src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextTransformer.kt rename to modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextTransformer.kt index 4386ce78..a78ca2a0 100644 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextTransformer.kt +++ b/modules/tools/ui/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/transformer/TextTransformer.kt @@ -1,4 +1,4 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer import com.intellij.icons.AllIcons import com.intellij.lang.Language @@ -23,14 +23,14 @@ import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.selected import com.intellij.ui.layout.not import com.intellij.util.ui.UIUtil -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor.EditorMode.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor.EditorMode.OUTPUT -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ValueProperty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration.PropertyType +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode.INPUT +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.common.AdvancedEditor.EditorMode.OUTPUT import java.awt.Dimension import javax.swing.JComponent @@ -39,35 +39,37 @@ abstract class TextTransformer( protected val context: DeveloperUiToolContext, protected val configuration: DeveloperToolConfiguration, parentDisposable: Disposable, - protected val project: Project? + protected val project: Project?, ) : DeveloperUiTool(parentDisposable), DeveloperToolConfiguration.ChangeListener { - // -- Properties -------------------------------------------------------------------------------------------------- // + // -- Properties ---------------------------------------------------------- // protected var liveTransformation = configuration.register("liveTransformation", true) - protected val sourceText = configuration.register("sourceText", "", PropertyType.INPUT, textTransformerContext.initialSourceExampleText) + protected val sourceText = + configuration.register( + "sourceText", + "", + PropertyType.INPUT, + textTransformerContext.initialSourceExampleText, + ) protected var resultText = ValueProperty("") private val sourceEditor by lazy { createSourceInputEditor() } private val resultEditor by lazy { createResultOutputEditor(parentDisposable) } - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // protected abstract fun transform() override fun Panel.buildUi() { buildTopConfigurationUi() - row { - cell(sourceEditor.component).align(Align.FILL) - }.resizableRow() + row { cell(sourceEditor.component).align(Align.FILL) }.resizableRow() buildMiddleConfigurationUi() buildActionsUi() - row { - cell(resultEditor.component).align(Align.FILL) - }.resizableRow() + row { cell(resultEditor.component).align(Align.FILL) }.resizableRow() } override fun afterBuildUi() { @@ -113,13 +115,12 @@ abstract class TextTransformer( configuration.removeChangeListener(this) } - // -- Private Methods --------------------------------------------------------------------------------------------- // + // -- Private Methods ----------------------------------------------------- // private fun Panel.buildActionsUi() { row { - val liveTransformationCheckBox = checkBox("Live transformation") - .bindSelected(liveTransformation) - .gap(RightGap.SMALL) + val liveTransformationCheckBox = + checkBox("Live transformation").bindSelected(liveTransformation).gap(RightGap.SMALL) button("â–¼ ${textTransformerContext.transformActionTitle}") { transform() } .enabledIf(liveTransformationCheckBox.selected.not()) @@ -127,10 +128,9 @@ abstract class TextTransformer( if (textTransformerContext.supportsDebug) { lateinit var debugButton: JComponent - debugButton = actionButton( - createDebugAction { debugButton }, - actionPlace = this::class.java.name - ).component + debugButton = + actionButton(createDebugAction { debugButton }, actionPlace = this::class.java.name) + .component } buildAdditionalActionsUi() @@ -141,11 +141,9 @@ abstract class TextTransformer( return object : DumbAwareAction("Debug", null, AllIcons.Toolwindows.ToolWindowDebugger) { override fun actionPerformed(e: AnActionEvent) { - val debugComponent = panel { - buildDebugComponent() - }.apply { - preferredSize = Dimension(300, preferredSize.height) - } + val debugComponent = + panel { buildDebugComponent() } + .apply { preferredSize = Dimension(300, preferredSize.height) } JBPopupFactory.getInstance() .createBalloonBuilder(createScrollPane(debugComponent, true)) .setDialogMode(true) @@ -162,34 +160,36 @@ abstract class TextTransformer( } } - private fun createSourceInputEditor(): DeveloperToolEditor = - DeveloperToolEditor( - id = "source-input", - context = context, - configuration = configuration, - project = project, - title = textTransformerContext.sourceTitle, - editorMode = INPUT, - parentDisposable = parentDisposable, - textProperty = sourceText, - diffSupport = textTransformerContext.diffSupport?.let { diffSupport -> - DeveloperToolEditor.DiffSupport( - title = diffSupport.title, - secondTitle = textTransformerContext.resultTitle, - secondText = { resultText.get() }, - ) - }, - initialLanguage = textTransformerContext.inputInitialLanguage ?: PlainTextLanguage.INSTANCE - ).apply { - onTextChangeFromUi { _ -> - if (liveTransformation.get()) { - transform() + private fun createSourceInputEditor(): AdvancedEditor = + AdvancedEditor( + id = "source-input", + context = context, + configuration = configuration, + project = project, + title = textTransformerContext.sourceTitle, + editorMode = INPUT, + parentDisposable = parentDisposable, + textProperty = sourceText, + diffSupport = + textTransformerContext.diffSupport?.let { diffSupport -> + AdvancedEditor.DiffSupport( + title = diffSupport.title, + secondTitle = textTransformerContext.resultTitle, + secondText = { resultText.get() }, + ) + }, + initialLanguage = textTransformerContext.inputInitialLanguage ?: PlainTextLanguage.INSTANCE, + ) + .apply { + onTextChangeFromUi { _ -> + if (liveTransformation.get()) { + transform() + } } } - } private fun createResultOutputEditor(parentDisposable: Disposable) = - DeveloperToolEditor( + AdvancedEditor( id = "result-output", context = context, configuration = configuration, @@ -198,17 +198,18 @@ abstract class TextTransformer( editorMode = OUTPUT, parentDisposable = parentDisposable, textProperty = resultText, - diffSupport = textTransformerContext.diffSupport?.let { diffSupport -> - DeveloperToolEditor.DiffSupport( - title = diffSupport.title, - secondTitle = textTransformerContext.sourceTitle, - secondText = { sourceText.get() }, - ) - }, - initialLanguage = textTransformerContext.outputInitialLanguage ?: PlainTextLanguage.INSTANCE + diffSupport = + textTransformerContext.diffSupport?.let { diffSupport -> + AdvancedEditor.DiffSupport( + title = diffSupport.title, + secondTitle = textTransformerContext.sourceTitle, + secondText = { sourceText.get() }, + ) + }, + initialLanguage = textTransformerContext.outputInitialLanguage ?: PlainTextLanguage.INSTANCE, ) - // -- Inner Type -------------------------------------------------------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // data class TextTransformerContext( val transformActionTitle: String, @@ -222,9 +223,7 @@ abstract class TextTransformer( val supportsDebug: Boolean = false, ) - data class DiffSupport( - val title: String - ) + data class DiffSupport(val title: String) - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/action.svg b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/action.svg similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/action.svg rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/action.svg diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/action_dark.svg b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/action_dark.svg similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/action_dark.svg rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/action_dark.svg diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter.svg b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter.svg similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter.svg rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter.svg diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter_dark.svg b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter_dark.svg similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter_dark.svg rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/clock_gutter_dark.svg diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu.svg b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu.svg similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu.svg rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu.svg diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu_dark.svg b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu_dark.svg similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu_dark.svg rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu_dark.svg diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/toolwindow.svg b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/toolwindow.svg similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/toolwindow.svg rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/toolwindow.svg diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/toolwindow_dark.svg b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/toolwindow_dark.svg similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/toolwindow_dark.svg rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/icons/toolwindow_dark.svg diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/lorem-ipsum.txt b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/lorem-ipsum.txt similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/lorem-ipsum.txt rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/lorem-ipsum.txt diff --git a/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/rubber-duck-yellow.png b/modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/rubber-duck-yellow.png similarity index 100% rename from src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/rubber-duck-yellow.png rename to modules/tools/ui/src/main/resources/dev/turingcomplete/intellijdevelopertoolsplugin/rubber-duck-yellow.png diff --git a/modules/tools/ui/src/main/resources/message/GeneralBundle.properties b/modules/tools/ui/src/main/resources/message/GeneralBundle.properties new file mode 100644 index 00000000..4773bbab --- /dev/null +++ b/modules/tools/ui/src/main/resources/message/GeneralBundle.properties @@ -0,0 +1,34 @@ +advanced-editor.soft-wrap=Soft-Wrap +advanced-editor.show-special-characters=Show Special Characters +advanced-editor.show-whitespaces=Show Whitespaces +advanced-editor.settings=Settings +advanced-editor.mode.input=input +advanced-editor.mode.output=output +advanced-editor.mode.input-output=input/output +advanced-editor.apply=Apply +advanced-editor.expand-editor-action-title=Expand Editor +advanced-editor.open-file-action-title=Open from File +advanced-editor.additional-actions=Additional Actions +advanced-editor.clipboard=Clipboard +advanced-editor.show-diff-with-clipboard=Show Diff with Clipboard +advanced-editor.diff-no-title-fallback=Content +advanced-editor.show-diff-with-title=Show Diff with {0} +advanced-editor.clear-content-action-title=Clear Content +advanced-editor.copy-to-clipboard-action-title=Copy to Clipboard +advanced-editor.save-to-file-action-title=Save to File +advanced-editor.file-saver-description=Save content as +file-handling.last-write.never=Last Write: Never +file-handling.error.no-file-selected=No file selected +file-handling.error.file-not-exist=File does not exist +file-handling.error.file-is-directory=File is a directory +file-handling.error.file-is-not-readable=File is not readable +file-handling.error.file-is-not-writable=File is not writable +file-handling.error.failed-to-write=Failed to write file +file-handling.last-write.details=Last Write: {0}, {1} +file-handling.drop-file-here=Drop file here to select +file-handling.file-chooser.title=Select File +file-handling.write-format.title=Write format: +file-handling.write-format.binary.title=Binary +file-handling.write-format.binary.description=Stores raw binary data directly without any encoding. +file-handling.write-format.hex.title=Hex +file-handling.write-format.hex.description=Encodes binary data as a hexadecimal string, commonly used for human-readable output and representing hashes like for example SHA-256. diff --git a/modules/tools/ui/src/main/resources/message/GeneralBundle_de.properties b/modules/tools/ui/src/main/resources/message/GeneralBundle_de.properties new file mode 100644 index 00000000..bb908d33 --- /dev/null +++ b/modules/tools/ui/src/main/resources/message/GeneralBundle_de.properties @@ -0,0 +1,34 @@ +advanced-editor.soft-wrap=Weicher Zeilenumbruch +advanced-editor.show-special-characters=Spezielle Zeichen anzeigen +advanced-editor.show-whitespaces=Leerzeichen anzeigen +advanced-editor.settings=Einstellungen +advanced-editor.mode.input=Eingabe +advanced-editor.mode.output=Ausgabe +advanced-editor.mode.input-output=Eingabe/Ausgabe +advanced-editor.apply=Anwenden +advanced-editor.expand-editor-action-title=Editor vergr\u00f6\u00dfern +advanced-editor.open-file-action-title=Aus Datei \u00f6ffnen +advanced-editor.additional-actions=Weitere Aktionen +advanced-editor.clipboard=Zwischenablage +advanced-editor.show-diff-with-clipboard=Unterschiede mit Zwischenablage anzeigen +advanced-editor.diff-no-title-fallback=Inhalt +advanced-editor.show-diff-with-title=Unterschiede mit {0} anzeigen +advanced-editor.clear-content-action-title=Inhalt leeren +advanced-editor.copy-to-clipboard-action-title=In Zwischenablage kopieren +advanced-editor.save-to-file-action-title=In Datei speichern +advanced-editor.file-saver-description=Inhalt speichern als +file-handling.last-write.never=Letzter Schreibvorgang: Nie +file-handling.error.no-file-selected=Keine Datei ausgew\u00E4hlt +file-handling.error.file-not-exist=Datei existiert nicht +file-handling.error.file-is-directory=Datei ist ein Verzeichnis +file-handling.error.file-is-not-readable=Datei ist nicht lesbar +file-handling.error.file-is-not-writable=Datei ist nicht schreibbar +file-handling.error.failed-to-write=Fehler beim Schreiben der Datei +file-handling.last-write.details=Letzter Schreibvorgang: {0}, {1} +file-handling.drop-file-here=Datei hierher ziehen, um sie auszuw\u00E4hlen +file-handling.file-chooser.title=Datei ausw\u00E4hlen +file-handling.write-format.title=Schreibformat: +file-handling.write-format.binary.title=Binär +file-handling.write-format.binary.description=Speichert rohe Bin\u00e4rdaten direkt ohne Kodierung. +file-handling.write-format.hex.title=Hex +file-handling.write-format.hex.description=Kodiert Bin\u00e4rdaten als hexadezimale Zeichenkette, h\u00e4ufig genutzt f\u00fcr menschenlesbare Ausgaben und die Darstellung von Hashes wie z.B. SHA-256. diff --git a/modules/tools/ui/src/main/resources/message/UiToolsBundle.properties b/modules/tools/ui/src/main/resources/message/UiToolsBundle.properties new file mode 100644 index 00000000..2990fc95 --- /dev/null +++ b/modules/tools/ui/src/main/resources/message/UiToolsBundle.properties @@ -0,0 +1,139 @@ +ascii-art.menu-title=ASCII Art +ascii-art.content-title=ASCII Art +ascii-art.font=Font: +ascii-art.example=Example +ascii-art.examples=Examples +ascii-art.output-title=ASCII Art +ascii-art.download-additional-ascii-art-fonts=Download ASCII Art Fonts From GitHub +ascii-art.download-additional-ascii-art-fonts-help=Download additional ASCII font files from the xero/figlet-fonts GitHub repository.

    Some font files may not work correctly and will be filtered out.

    These additional font files are not part of this plugin and are subject to individual licences. +ascii-art.download-additional-ascii-art-fonts-failed-title=Download Files From GitHub +ascii-art.download-additional-ascii-art-fonts-failed-details=Not all files could be downloaded. See idea.log for more details. +server-certificates.menu-title=Server Certificates +server-certificates.content-title=Server Certificates +server-certificates.url=URL: +server-certificates.follow-redirects=Follow redirects +server-certificates.allow-insecure-connection=Allow insecure connection +server-certificates.allow-insecure-connection-help=Enabling this option permits connections to servers that have invalid server certificates.

    Use this option if you get any SSLHandshakeException errors. +server-certificates.fetch-server-certificates=Fetch Server Certificates +server-certificates.fetch-server-certificates-in-progress=Fetching server certificates... +server-certificates.fetch-server-certificates-in-progress-title=Fetching server certificates +server-certificates.fetch-server-certificates-failed=Failed to retrieve server certificates: {0} +server-certificates.result=Server Certificates +server-certificates.response=Response: {0} {1} +server-certificates.no-result=Connection didn't use any server certificates +server-certificates.certificate-title=Server Certificate {0} +server-certificates.certificate-expired=Certificate is expired +server-certificates.certificate-not-valid-yet=Certificate is not yet valid +server-certificates.certificate-subject=Subject +server-certificates.certificate-issuer=Issuer +server-certificates.certificate-serial-number=Serial Number +server-certificates.certificate-valid-from=Valid From +server-certificates.certificate-valid-to=Valid To +server-certificates.certificate-signature-algorithm=Signature Algo. +server-certificates.export-action-title=Export as {0} +server-certificates.export-failed=Failed to export certificate(s): {0} +server-certificates.export-jks-result=Certificates exported as JKS with password: {0} +server-certificates.copy-pem-to-clipboard-action-title=Copy as PEM to Clipboard +server-certificates.show-as-pem-action-title=Show as PEM +server-certificates.show-certificate-details-action-title=Show Certificate Details +regular-expression-matcher.text-input-title=Text +regular-expression-matcher.menu-title=Regular Expression +regular-expression-matcher.content-title=Regular Expression Matcher +regular-expression-matcher.regex-input=Regular expression: +regular-expression-matcher.replace-pattern-context-help=A matching group can be referenced using a dollar sign followed by the group's index number. For example, $1 refers to the first matching group.

    The group index $0 represents the entire matched text.

    To avoid ambiguity when referencing groups, curly brackets can be used to delimit the index number. For instance, ${1}3 explicitly refers to the group with index 1, followed by the character "3". +regular-expression-matcher.matches-title=Matches +regular-expression-matcher.matches-match-prefix=Match: +regular-expression-matcher.matches-group-prefix=Group: +regular-expression-matcher.matches-no-matches=No matches +regular-expression-matcher.matches-group=Group +regular-expression-matcher.matches-value=Value +regular-expression-matcher.extraction-title=Extraction +regular-expression-matcher.substitution-title=Substitution +converter.live-conversion=Live conversion +converter.file-input-output-handler.title=File +converter.text-input-output-handler.simple-title=Text +converter.text-input-output-handler.hex-title=Text (Hex) +encoder-decoder.source-title=Decoded +encoder-decoder.target-title=Encoded +encoder-decoder.to-target-title=Encode +encoder-decoder.to-source-title=Decode +url-encoding.menu-title=URL Encoding +url-encoding.grouped-menu-title=URL +url-encoding.title=URL Encoding Encoder/Decoder +ascii-encoding.title=ASCII Encoder/Decoder +ascii-encoding.grouped-menu-title=ASCII +ascii-encoding.menu-title=ASCII Encoding +base32-encoding.menu-title=Base32 Encoding +base32-encoding.grouped-menu-title=Base32 +base32-encoding.title=Base32 Encoder/Decoder +base64-encoding.menu-title=Base64 Encoding +base64-encoding.grouped-menu-title=Base64 +base64-encoding.title=Base64 Encoder/Decoder +url-base64-encoding.menu-title=URL Base64 Encoding +url-base64-encoding.grouped-menu-title=URL Base64 +url-base64-encoding.title=URL Base64 Encoder/Decoder +mime-base64-encoding.menu-title=MIME Base64 Encoding +mime-base64-encoding.grouped-menu-title=MIME Base64 +mime-base64-encoding.title=MIME Base64 Encoder/Decoder +code-formatting.title=Text Format +code-formatting.content-title=Text Format Converter +code-formatting.json-title=JSON +code-formatting.yaml-title=YAML +code-formatting.xml-title=XML +code-formatting.properties-title=Properties +code-formatting.toml-title=TOML +code-formatting.first-language=First language: +code-formatting.second-language=Second language: +code-formatting.first-title=First +code-formatting.second-title=Second +code-formatting.to-first-title=Convert +code-formatting.to-source-title=Convert +escaper-unescaper.target-title=Escaped +escaper-unescaper.source-title=Unescaped +escaper-unescaper.to-target-title=Escape +escaper-unescaper.to-source-title=Unescape +escape-sequences-escaper-unescaper.title=Escape Sequences +escape-sequences-escaper-unescaper.content-title=Escape Sequences Escaper/Unescaper +escape-sequences-escaper-unescaper.context-help=Escapes or unescaped line breaks, tabs, backslashes, single quotes and double quotes. +escape-sequences-escaper-unescaper.do-line-break-decoding=Line breaks: +escape-sequences-escaper-unescaper.do-tab-decoding=Tabs (\\t) +escape-sequences-escaper-unescaper.do-backslash-decoding=Backslashes (\\\) +escape-sequences-escaper-unescaper.do-single-quote-decoding=Single quotes (\\') +escape-sequences-escaper-unescaper.do-double-quote-decoding=Double quotes (\\") +escape-sequences-escaper-unescaper.settings.title=Settings +escape-sequences-escaper-unescaper.settings.escape-sequences-to-be-decoded=Escape sequences to be escaped/unescaped +cli-command-converter.title=CLI Command +cli-command-converter.content-title=CLI Command Line Breaks +cli-command-converter.source-title=Without line breaks +cli-command-converter.target-title=With line breaks +cli-command-converter.to-source-title=Remove +cli-command-converter.to-target-title=Add +cli-command-converter.line-break-delimiter=Line break delimiter: +html-entities-escaper-unescaper.title=HTML Entities Escaping +html-entities-escaper-unescaper.grouped-menu-title=HTML Entities +html-entities-escaper-unescaper.content-title=HTML Entities Escaper/Unescaper +html-entities-escaper-unescaper.description.context-help=This tool will use StringEscapeUtils.escapeHtml4(text) and StringEscapeUtils.unescapeHtml4(text) from the 'Apache Commons Text' library. +java-string-escaper-unescaper.title=Java String Escaping +java-string-escaper-unescaper.grouped-menu-title=Java String +java-string-escaper-unescaper.content-title=Java String Escaper/Unescaper +java-string-escaper-unescaper.description.context-help=This tool will use StringEscapeUtils.escapeJava(text) and StringEscapeUtils.unescapeJava(text) from the 'Apache Commons Text' library. +json-text-escaper-unescaper.title=JSON Text Escaping +json-text-escaper-unescaper.grouped-menu-title=JSON Text +json-text-escaper-unescaper.content-title=JSON Text Escaper/Unescaper +json-text-escaper-unescaper.description.context-help=This tool will use StringEscapeUtils.escapeJson(text) and StringEscapeUtils.unescapeJson(text) from the 'Apache Commons Text' library. +csv-text-escaper-unescaper.title=CSV Text Escaping +csv-text-escaper-unescaper.grouped-menu-title=CSV Text +csv-text-escaper-unescaper.content-title=CSV Text Escaper/Unescaper +csv-text-escaper-unescaper.description.context-help=This tool will use StringEscapeUtils.escapeCsv(text) and StringEscapeUtils.unescapeCsv(text) from the 'Apache Commons Text' library. +xml-text-escaper-unescaper.title=XML Text Escaping +xml-text-escaper-unescaper.content-title=XML Text Escaper/Unescaper +xml-text-escaper-unescaper.grouped-menu-title=XML Text +xml-text-escaper-unescaper.description.context-help=This tool will use StringEscapeUtils.escapeXml11(text) and StringEscapeUtils.unescapeXml(text) from the 'Apache Commons Text' library. +color-picker.css-colors.title=CSS Colors +color-picker.parse-css-color-action.title=Parse CSS Color Value +color-picker.parse-css-color-action.input=CSS color value: +color-picker.settings.title=Settings +color-picker.settings.decimal-places=Decimal places: +color-picker.parse-css-color-action.error.message=Unable to parse CSS value. +color-picker.title=Color Picker +color-picker.content-title=Color Picker diff --git a/modules/tools/ui/src/main/resources/message/UiToolsBundle_de.properties b/modules/tools/ui/src/main/resources/message/UiToolsBundle_de.properties new file mode 100644 index 00000000..fbbf168d --- /dev/null +++ b/modules/tools/ui/src/main/resources/message/UiToolsBundle_de.properties @@ -0,0 +1,139 @@ +ascii-art.menu-title=ASCII-Art +ascii-art.content-title=ASCII-Art +ascii-art.font=Schriftart: +ascii-art.example=Beispiel +ascii-art.examples=Beispiele +ascii-art.output-title=ASCII-Art +ascii-art.download-additional-ascii-art-fonts=ASCII-Art-Schriftarten von GitHub herunterladen +ascii-art.download-additional-ascii-art-fonts-help=Zus\u00e4tzliche ASCII-Schriftartdateien aus dem xero/figlet-fonts-GitHub-Repository herunterladen.

    Einige Schriftartdateien funktionieren m\u00f6glicherweise nicht korrekt und werden herausgefiltert.

    Diese zus\u00e4tzlichen Schriftartdateien sind nicht Teil dieses Plugins und unterliegen individuellen Lizenzen. +ascii-art.download-additional-ascii-art-fonts-failed-title=GitHub-Dateien Herunterladen +ascii-art.download-additional-ascii-art-fonts-failed-details=Nicht alle Dateien konnten heruntergeladen werden. Weitere Details finden Sie in dem idea.log. +server-certificates.menu-title=Server-Zertifikate +server-certificates.content-title=Server-Zertifikate +server-certificates.url=URL: +server-certificates.follow-redirects=Weiterleitungen folgen +server-certificates.allow-insecure-connection=Unsichere Verbindung zulassen +server-certificates.allow-insecure-connection-help=Wenn diese Option aktiviert ist, werden Verbindungen zu Servern mit ung\u00fcltigen Serverzertifikaten zugelassen.

    Verwenden Sie diese Option, wenn SSLHandshakeException-Fehler auftreten. +server-certificates.fetch-server-certificates=Server-Zertifikate abrufen +server-certificates.fetch-server-certificates-in-progress=Server-Zertifikate werden abgerufen... +server-certificates.fetch-server-certificates-in-progress-title=Server-Zertifikate werden abgerufen +server-certificates.fetch-server-certificates-failed=Fehler beim Abrufen der Server-Zertifikate: {0} +server-certificates.result=Server-Zertifikate +server-certificates.response=Antwort: {0} {1} +server-certificates.no-result=Die Verbindung hat keine Server-Zertifikate verwendet +server-certificates.certificate-title=Server Zertifikat {0} +server-certificates.certificate-expired=Zertifikat ist abgelaufen +server-certificates.certificate-not-valid-yet=Zertifikat ist noch nicht g\u00fcltig +server-certificates.certificate-subject=Subject +server-certificates.certificate-issuer=Aussteller +server-certificates.certificate-serial-number=Seriennummer +server-certificates.certificate-valid-from=G\u00fcltig ab +server-certificates.certificate-valid-to=G\u00fcltig bis +server-certificates.certificate-signature-algorithm=Signatur-Algo. +server-certificates.export-action-title=Zertifikat(e) als {0} exportieren +server-certificates.export-failed=Fehler beim Export der Zertifikate: {0} +server-certificates.export-jks-result=Zertifikate wurden als JKS mit dem Passwort {0} exportiert +server-certificates.copy-pem-to-clipboard-action-title=Als PEM in die Zwischenablage kopieren +server-certificates.show-as-pem-action-title=Als PEM anzeigen +server-certificates.show-certificate-details-action-title=Zertifikatsdetails anzeigen +regular-expression-matcher.text-input-title=Text +regular-expression-matcher.menu-title=Regul\u00e4rer Ausdruck +regular-expression-matcher.content-title=Regul\u00e4rer Ausdruck Matcher +regular-expression-matcher.matches-title=Treffer +regular-expression-matcher.matches-match-prefix=Treffer: +regular-expression-matcher.matches-group-prefix=Gruppe: +regular-expression-matcher.matches-no-matches=Keine Treffer +regular-expression-matcher.regex-input=Regul\u00e4rer Ausdruck: +regular-expression-matcher.replace-pattern-context-help=Ein \u00fcbereinstimmender Group kann mit einem Dollarzeichen, gefolgt von der Indexnummer der Gruppe, referenziert werden. Zum Beispiel bezieht sich $1 auf die erste \u00fcbereinstimmende Gruppe.

    Der Gruppenindex $0 repr\u00e4sentiert den gesamten \u00fcbereinstimmenden Text.

    Um Mehrdeutigkeiten zu vermeiden, k\u00f6nnen geschweifte Klammern verwendet werden, um die Indexnummer zu begrenzen. Zum Beispiel bezieht sich ${1}3 explizit auf die Gruppe mit Index 1, gefolgt von der Zahl "3". +regular-expression-matcher.matches-group=Gruppe +regular-expression-matcher.matches-value=Wert +regular-expression-matcher.substitution-title=Ersetzung +regular-expression-matcher.extraction-title=Extraktion +converter.live-conversion=Live-Konvertierung +converter.file-input-output-handler.title=Datei +converter.text-input-output-handler.simple-title=Text +converter.text-input-output-handler.hex-title=Text (Hex) +encoder-decoder.source-title=Entschl\u00fcsselt +encoder-decoder.target-title=Verschl\u00fcsselt +encoder-decoder.to-target-title=Verschl\u00fcsseln +encoder-decoder.to-source-title=Entschl\u00fcsseln +url-encoding.menu-title=URL-Kodierung +url-encoding.grouped-menu-title=URL +url-encoding.title=URL-Kodierer/Decoder +ascii-encoding.title=ASCII-Kodierer/Decoder +ascii-encoding.grouped-menu-title=ASCII +ascii-encoding.menu-title=ASCII-Kodierung +base32-encoding.menu-title=Base32-Kodierung +base32-encoding.grouped-menu-title=Base32 +base32-encoding.title=Base32-Kodierer/Decoder +base64-encoding.menu-title=Base64-Kodierung +base64-encoding.grouped-menu-title=Base64 +base64-encoding.title=Base64-Kodierer/Decoder +url-base64-encoding.menu-title=URL-Base64-Kodierung +url-base64-encoding.grouped-menu-title=URL Base64 +url-base64-encoding.title=URL-Base64-Kodierer/Decoder +mime-base64-encoding.menu-title=MIME-Base64-Kodierung +mime-base64-encoding.grouped-menu-title=MIME Base64 +mime-base64-encoding.title=MIME-Base64-Kodierer/Decoder +code-formatting.title=Textformat +code-formatting.content-title=Textformat-Konverter +code-formatting.json-title=JSON +code-formatting.yaml-title=YAML +code-formatting.xml-title=XML +code-formatting.properties-title=Properties +code-formatting.toml-title=TOML +code-formatting.first-language=Erste Sprache: +code-formatting.second-language=Zweite Sprache: +code-formatting.first-title=Erste +code-formatting.second-title=Zweite +code-formatting.to-first-title=Konvertieren +code-formatting.to-source-title=Konvertieren +escaper-unescaper.target-title=Escape +escaper-unescaper.source-title=Unescape +escaper-unescaper.to-target-title=Escapen +escaper-unescaper.to-source-title=Unescapen +escape-sequences-escaper-unescaper.title=Escape-Sequenzen +escape-sequences-escaper-unescaper.content-title=Escape-Sequenzen Escaper/Unescaper +escape-sequences-escaper-unescaper.context-help=Escapes oder unescapes Zeilenumbrüche, Tabulatoren, Rückwärtsschrägstriche, einfache Anführungszeichen und doppelte Anführungszeichen. +escape-sequences-escaper-unescaper.do-line-break-decoding=Zeilenumbr\u00fcche: +escape-sequences-escaper-unescaper.do-tab-decoding=Tabulatoren (\\t) +escape-sequences-escaper-unescaper.do-backslash-decoding=Backslashes (\\\) +escape-sequences-escaper-unescaper.do-single-quote-decoding=Einfache Anf\u00fchrungszeichen (\\') +escape-sequences-escaper-unescaper.do-double-quote-decoding=Doppelte Anf\u00fchrungszeichen (\\") +escape-sequences-escaper-unescaper.settings.title=Einstellungen +escape-sequences-escaper-unescaper.settings.escape-sequences-to-be-decoded=Zu escapende/unescapende Sequenzen +cli-command-converter.title=CLI-Befehl +cli-command-converter.content-title=CLI-Befehl-Zeilenumbr\u00fcche +cli-command-converter.source-title=Ohne Zeilenumbr\u00fcche +cli-command-converter.target-title=Mit Zeilenumbr\u00fcche +cli-command-converter.to-source-title=Entfernen +cli-command-converter.to-target-title=Hinzuf\u00fcgen +cli-command-converter.line-break-delimiter=Zeilenumbruch-Trennzeichen: +html-entities-escaper-unescaper.title=HTML-Entities Escaping +html-entities-escaper-unescaper.grouped-menu-title=HTML-Entities +html-entities-escaper-unescaper.content-title=HTML-Entities Escaper/Unescaper +html-entities-escaper-unescaper.description.context-help=Dieses Tool verwendet StringEscapeUtils.escapeHtml4(text) und StringEscapeUtils.unescapeHtml4(text) aus der Bibliothek 'Apache Commons Text'. +java-string-escaper-unescaper.title=Java-String Escaping +java-string-escaper-unescaper.grouped-menu-title=Java-String +java-string-escaper-unescaper.content-title=Java-String Escaper/Unescaper +java-string-escaper-unescaper.description.context-help=Dieses Tool verwendet StringEscapeUtils.escapeJava(text) und StringEscapeUtils.unescapeJava(text) aus der Bibliothek 'Apache Commons Text'. +json-text-escaper-unescaper.title=JSON-Text Escaping +json-text-escaper-unescaper.grouped-menu-title=JSON-Text +json-text-escaper-unescaper.content-title=JSON-Text Escaper/Unescaper +json-text-escaper-unescaper.description.context-help=Dieses Tool verwendet StringEscapeUtils.escapeJson(text) und StringEscapeUtils.unescapeJson(text) aus der Bibliothek 'Apache Commons Text'. +csv-text-escaper-unescaper.title=CSV-Text Escaping +csv-text-escaper-unescaper.grouped-menu-title=CSV-Text +csv-text-escaper-unescaper.content-title=CSV-Text Escaper/Unescaper +csv-text-escaper-unescaper.description.context-help=Dieses Tool verwendet StringEscapeUtils.escapeCsv(text) und StringEscapeUtils.unescapeCsv(text) aus der Bibliothek 'Apache Commons Text'. +xml-text-escaper-unescaper.title=XML-Text Escaping +xml-text-escaper-unescaper.content-title=XML-Text Escaper/Unescaper +xml-text-escaper-unescaper.grouped-menu-title=XML-Text +xml-text-escaper-unescaper.description.context-help=Dieses Tool verwendet StringEscapeUtils.escapeXml11(text) und StringEscapeUtils.unescapeXml(text) aus der Bibliothek 'Apache Commons Text'. +color-picker.css-colors.title=CSS-Farben +color-picker.parse-css-color-action.title=CSS-Farbwert umwandeln +color-picker.parse-css-color-action.input=CSS-Farbwert: +color-picker.settings.title=Einstellungen +color-picker.settings.decimal-places=Dezimalstellen: +color-picker.parse-css-color-action.error.message=CSS-Wert konnte nicht umwandelt werden. +color-picker.title=Farbw\u00e4hler +color-picker.content-title=Farbw\u00e4hler diff --git a/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/UiBundleMessagesTest.kt b/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/UiBundleMessagesTest.kt new file mode 100644 index 00000000..e563b853 --- /dev/null +++ b/modules/tools/ui/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/UiBundleMessagesTest.kt @@ -0,0 +1,34 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui + +import com.intellij.testFramework.junit5.RunMethodInEdt +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.BundleMessagesTest +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.TestFactory + +class UiBundleMessagesTest : BundleMessagesTest() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @TestFactory + override fun `test that all additional languages are containing the same message keys and parameter counts`(): + List = + super + .`do test that all additional languages are containing the same message keys and parameter counts`() + + @TestFactory + @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) + override fun `test that all message(messageKey, params) calls are referencing to an existing message key and are using the correct parameters count`(): + List = + super + .`do test that all message(messageKey, params) calls are referencing to an existing message key and are using the correct parameters count`() + + @TestFactory + @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) + override fun `test that all keys in the messages bundle are used`(): List = + super.`do test that all keys in the messages bundle are used`() + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/ConverterDeveloperUiToolUnderTest.kt b/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/ConverterDeveloperUiToolUnderTest.kt new file mode 100644 index 00000000..0fd41245 --- /dev/null +++ b/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/ConverterDeveloperUiToolUnderTest.kt @@ -0,0 +1,33 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.testfixtures + +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactoryEp +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.Converter + +class ConverterDeveloperUiToolUnderTest( + factoryEp: DeveloperUiToolFactoryEp>, + configuration: DeveloperToolConfiguration, + instance: Converter, +) : + DeveloperUiToolUnderTest( + factoryEp = factoryEp, + configuration = configuration, + instance = instance, + ) { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + override fun randomisePropertyValue(property: DeveloperToolConfiguration.PropertyContainer): Any = + when (property.key) { + "sourceText" -> randomString() + "targetText" -> + String(instance.doConvertToTarget(getStringPropertyValue("sourceText").toByteArray())) + else -> super.randomisePropertyValue(property) + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolUnderTest.kt b/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolUnderTest.kt new file mode 100644 index 00000000..19f5c3c4 --- /dev/null +++ b/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolUnderTest.kt @@ -0,0 +1,155 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.testfixtures + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.testFramework.runInEdtAndWait +import com.intellij.ui.JBColor +import dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer +import dev.turingcomplete.intellijdevelopertoolsplugin.common.random +import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactory +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactoryEp +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.JwtEncoderDecoder +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.transformer.HmacTransformer +import java.math.BigDecimal +import java.security.Security +import java.time.ZoneId +import java.util.Locale +import kotlin.random.Random + +open class DeveloperUiToolUnderTest( + val factoryEp: DeveloperUiToolFactoryEp>, + val configuration: DeveloperToolConfiguration, + val instance: T, +) { + // -- Properties ---------------------------------------------------------- // + + val id: String = factoryEp.id + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun randomiseConfiguration() { + runInEdtAndWait { + configuration.properties.forEach { key, property -> + val randomValue = randomisePropertyValue(property) + property.reference.setWithUncheckedCast(randomValue, null) + } + } + } + + fun resetConfiguration(loadExamples: Boolean) { + runInEdtAndWait { configuration.reset(null, loadExamples) } + } + + fun modifyAllConfigurationProperties( + modify: (DeveloperToolConfiguration.PropertyContainer) -> Any? + ) { + ApplicationManager.getApplication() + .invokeAndWait( + { + configuration.properties.values.forEach { + val modifiedValue = modify(it) + if (modifiedValue != null) { + it.reference.setWithUncheckedCast(modifiedValue, null) + } + } + }, + ModalityState.any(), + ) + } + + protected open fun randomisePropertyValue( + property: DeveloperToolConfiguration.PropertyContainer + ): Any = + when { + property.key == "liveConversion" -> false + + id == "code-style-formatting" && property.key == "languageId" -> "XML" + id == "code-style-formatting" && property.key == "sourceText" -> "" + id == "code-style-formatting" && property.key == "targetText" -> "" + + id == "ascii-art" && property.key == "selectedFontFileName" -> "slant.flf" + + id == "hashing-transformer" && property.key == "algorithm" -> + Security.getAlgorithms("MessageDigest").random { it == property.defaultValue } + + id == "hmac-transformer" && property.key == "algorithm" -> + HmacTransformer.hmacAlgorithms.random { it.algorithm == property.defaultValue }.algorithm + + id == "units-converter" && property.key == "baseConverter_baseTwoInput" -> + randomBaseTwoString() + + id == "date-time-converter" && property.key == "timeZoneId" -> + ZoneId.getAvailableZoneIds().random { it == property.defaultValue } + + id == "jwt-encoder-decoder" && property.key == "algorithm" -> + JwtEncoderDecoder.SignatureAlgorithm.HMAC512 + id == "jwt-encoder-decoder" && property.key == "encodedText" -> + "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.ANCf_8p1AE4ZQs7QuqGAyyfTEgYrKSjKWkhBk5cIn1_2QVr2jEjmM-1tu7EgnyOf_fAsvdFXva8Sv05iTGzETg" + id == "jwt-encoder-decoder" && property.key == "headerText" -> + """ + { + "alg": "HS512", + "typ": "JWT" + } + """ + .trimIndent() + id == "jwt-encoder-decoder" && property.key == "payloadText" -> + """ + { + "sub": "1234567890", + "name": "John Doe", + "admin": true, + "iat": 1516239022 + } + """ + .trimIndent() + + else -> getRandomPropertyValueBasedOnType(property) + } + + private fun getRandomPropertyValueBasedOnType( + property: DeveloperToolConfiguration.PropertyContainer + ): Any { + val defaultValue = property.defaultValue + return when (defaultValue) { + is String -> randomString() + is Int -> defaultValue + 1 + is Long -> defaultValue + 1 + is BigDecimal -> defaultValue.plus(BigDecimal.ONE) + is Boolean -> !defaultValue + is LocaleContainer -> + LocaleContainer(Locale.getAvailableLocales().random { it == defaultValue.locale }) + + is Enum<*> -> { + val enumConstants = defaultValue::class.java.enumConstants + enumConstants[(defaultValue.ordinal + 1) % enumConstants.size] + } + + is JBColor -> JBColor(Random.nextInt(0, 255), Random.nextInt(0, 255)) + + else -> + throw IllegalStateException("Missing property type mapping for: " + defaultValue::class) + } + } + + protected fun randomString(): String { + val allowedChars = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return (1..20).map { allowedChars.random() }.joinToString("") + } + + protected fun getStringPropertyValue(key: String): String = + configuration.properties[key]?.reference?.get()?.safeCastTo() + ?: error("Unknown property key: $key") + + // -- Private Methods ----------------------------------------------------- // + + private fun randomBaseTwoString(): String = + (1..Random.nextInt(3, 9)).map { Random.nextInt(0, 2) }.joinToString("") + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolsInstances.kt b/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolsInstances.kt new file mode 100644 index 00000000..f7a17d69 --- /dev/null +++ b/modules/tools/ui/src/testFixtures/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/tool/ui/testfixtures/DeveloperUiToolsInstances.kt @@ -0,0 +1,68 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.testfixtures + +import com.intellij.openapi.Disposable +import com.intellij.openapi.project.Project +import com.intellij.testFramework.runInEdtAndWait +import com.intellij.util.application +import dev.turingcomplete.intellijdevelopertoolsplugin.common.ifNotEmpty +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiTool +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolFactoryEp +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.base.Converter + +object DeveloperUiToolsInstances { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + fun createDeveloperUiToolsUnderTest( + project: Project, + parentDisposable: Disposable, + settings: DeveloperToolsInstanceSettings, + ): List> { + val developerUiToolsUnderTest = mutableListOf>() + + DeveloperUiToolFactoryEp.EP_NAME.forEachExtensionSafe { factoryEp -> + val configuration = + settings.getDeveloperToolConfigurations(factoryEp.id).ifNotEmpty { first() } + ?: settings.createDeveloperToolConfiguration(factoryEp.id) + + val context = DeveloperUiToolContext(factoryEp.id, false) + val instance: DeveloperUiTool = + factoryEp + .createInstance(application) + .getDeveloperUiToolCreator(project, parentDisposable, context) + ?.invoke(configuration) + ?: error("No instance of tool `${factoryEp.id}` was be created by its factory") + configuration.wasConsumedByDeveloperTool = true + runInEdtAndWait { + instance.createComponent() + instance.activated() + } + + val developerUiToolUnderTest = + when { + instance is Converter -> + ConverterDeveloperUiToolUnderTest( + factoryEp = factoryEp, + configuration = configuration, + instance = instance, + ) + + else -> + DeveloperUiToolUnderTest( + factoryEp = factoryEp, + configuration = configuration, + instance = instance, + ) + } + developerUiToolsUnderTest.add(developerUiToolUnderTest) + } + + return developerUiToolsUnderTest + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 85600c31..58ff4ebb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,13 +1,31 @@ +import java.nio.file.Paths + rootProject.name = "intellij-developer-tools-plugin" val platform = settings.extra["platform"] -val modules = mutableListOf("common") +val modules = mutableSetOf( + Module("common"), + Module("settings"), + Module("tools-editor", Paths.get("modules/tools/editor")), + Module("tools-ui", Paths.get("modules/tools/ui")) +) if (platform == "IC") { - modules.add("java-dependent") - modules.add("kotlin-dependent") + modules.add(Module("java-dependent")) + modules.add(Module("kotlin-dependent")) +} +modules.forEach { module -> + include(module.name) + project(":${module.name}").projectDir = file(module.directory) } -modules.forEach { projectName -> - include(projectName) - project(":$projectName").projectDir = file("modules/$projectName") + +data class Module( + val name: String, + val directory: java.nio.file.Path = Paths.get("modules/$name") +) + +pluginManagement { + repositories { + gradlePluginPortal() + } } diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/DeveloperToolsPluginProjectActivity.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/DeveloperToolsPluginProjectActivity.kt deleted file mode 100644 index 08ea916d..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/DeveloperToolsPluginProjectActivity.kt +++ /dev/null @@ -1,26 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.startup.ProjectActivity -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.AddOpenMainDialogActionToMainToolbarTask - -class DeveloperToolsPluginProjectActivity : ProjectActivity { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override suspend fun execute(project: Project) { - if (DeveloperToolsApplicationSettings.instance.addOpenMainDialogActionToMainToolbar) { - val addOpenMainDialogActionToMainToolbarTask = AddOpenMainDialogActionToMainToolbarTask.createIfAvailable() - if (addOpenMainDialogActionToMainToolbarTask != null) { - ApplicationManager.getApplication().invokeLater { addOpenMainDialogActionToMainToolbarTask.run() } - } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/DeveloperUiToolFactoryEp.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/DeveloperUiToolFactoryEp.kt deleted file mode 100644 index 2ac8056f..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/DeveloperUiToolFactoryEp.kt +++ /dev/null @@ -1,43 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal - -import com.intellij.openapi.extensions.CustomLoadingExtensionPointBean -import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.extensions.RequiredElement -import com.intellij.util.xmlb.annotations.Attribute -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory - -class DeveloperUiToolFactoryEp> : CustomLoadingExtensionPointBean() { - // -- Properties -------------------------------------------------------------------------------------------------- // - - @Attribute("id") - @RequiredElement - lateinit var id: String - - @Attribute("implementationClass") - @RequiredElement - lateinit var implementationClass: String - - @Attribute("preferredSelected") - var preferredSelected: Boolean = false - - @Attribute("groupId") - var groupId: String? = null - - @Attribute("internalTool") - var internalTool: Boolean = false - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun getImplementationClassName(): String = implementationClass - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - val EP_NAME: ExtensionPointName>> = - ExtensionPointName.create("dev.turingcomplete.intellijdevelopertoolsplugins.developerUiTool") - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/DefaultObjectMapper.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/DefaultObjectMapper.kt deleted file mode 100644 index 977069d1..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/DefaultObjectMapper.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common - -import com.fasterxml.jackson.core.util.DefaultIndenter -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper - -// -- Properties -------------------------------------------------------------------------------------------------- // - -private const val OBJECT_MAPPER_INDENT = " " -private val indenter = DefaultIndenter(OBJECT_MAPPER_INDENT, System.lineSeparator()) -private val prettyPrinter = DefaultPrettyPrinter() - .withObjectIndenter(indenter) - .withArrayIndenter(indenter) -val objectMapper: ObjectMapper = ObjectMapper() - .setDefaultPrettyPrinter(prettyPrinter) - -fun JsonNode.toPrettyStringWithDefaultObjectMapper(): String { - if (this.isMissingNode) { - return "" - } - - return objectMapper - .writerWithDefaultPrettyPrinter() - .writeValueAsString(this) -} - -// -- Exposed Methods --------------------------------------------------------------------------------------------- // -// -- Private Methods --------------------------------------------------------------------------------------------- // -// -- Inner Type -------------------------------------------------------------------------------------------------- // \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ErrorHolder.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ErrorHolder.kt deleted file mode 100644 index 71509d1f..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/ErrorHolder.kt +++ /dev/null @@ -1,114 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common - -import com.intellij.openapi.observable.properties.ObservableMutableProperty -import com.intellij.openapi.ui.ValidationInfo -import com.intellij.ui.layout.ComponentPredicate -import com.intellij.ui.layout.ValidationInfoBuilder -import javax.swing.JComponent - -class ErrorHolder( - // Icon must be used in a `text()` cell, a `label()` cell will not work. - private val addErrorIconToMessage: Boolean = false, - private val surroundMessageWithHtml: Boolean = true -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val changeListeners = mutableListOf<(List) -> Unit>() - - private var errors = mutableListOf() - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - fun add(prefix: String, error: Throwable) { - errors.add("$prefix ${error.message ?: error::class.java.simpleName}") - fireChange() - } - - fun add(error: Throwable) { - errors.add(error.message ?: error::class.java.simpleName) - fireChange() - } - - fun add(message: String) { - errors.add(message) - fireChange() - } - - fun addIfEmpty(message: String) { - if (errors.isNotEmpty()) { - return - } - errors.add(message) - fireChange() - } - - fun clear() { - errors.clear() - fireChange() - } - - fun isSet() = errors.isNotEmpty() - - fun isNotSet() = errors.isEmpty() - - fun asComponentPredicate() = object : ComponentPredicate() { - - override fun addListener(listener: (Boolean) -> Unit) { - changeListeners.add { listener(it.isNotEmpty()) } - } - - override fun invoke(): Boolean = errors.isNotEmpty() - } - - fun asValidation(forComponent: JComponent? = null): ValidationInfoBuilder.(T) -> ValidationInfo? { - return { formatErrors()?.let { ValidationInfo(it, forComponent) } } - } - - /** - * Creates a [ObservableMutableProperty] that will return an empty string if - * there is no error. - * - * The `set()` operation is not supported, but a [com.intellij.ui.dsl.builder.Row.text] - * requires [ObservableMutableProperty] and not only a - * [com.intellij.openapi.observable.properties.ObservableProperty]. - */ - fun asPropertyForTextCell(): ObservableMutableProperty = object : ObservableMutableProperty { - - override fun afterChange(listener: (String) -> Unit) { - changeListeners.add { listener(formatErrors() ?: "") } - } - - override fun get(): String = formatErrors() ?: "" - - override fun set(value: String) { - throw UnsupportedOperationException() - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun formatErrors(): String? = errors.let { - when (it.size) { - 0 -> null - 1 -> if (surroundMessageWithHtml) "${formatError(it[0])}" else formatError(it[0]) - else -> it.joinToString( - separator = "\n", - prefix = "${if (surroundMessageWithHtml) "" else ""}
      ", - postfix = "
    ${if (surroundMessageWithHtml) "" else ""}" - ) { error -> - "
  • ${formatError(error)}
  • " - } - } - } - - private fun formatError(message: String) = - "${if (addErrorIconToMessage) " " else ""}$message" - - private fun fireChange() { - changeListeners.forEach { it(errors) } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/GitHubUtils.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/GitHubUtils.kt deleted file mode 100644 index 83bdfe35..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/GitHubUtils.kt +++ /dev/null @@ -1,147 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.Task -import com.intellij.openapi.project.Project -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.OkHttpClientUtils.applyIntelliJProxySettings -import okhttp3.OkHttpClient -import okhttp3.Request -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption - -object GitHubUtils { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val log = logger() - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - fun downloadFiles( - project: Project, - repositoryUrl: String, - destinationPath: Path, - preDownloadFilter: (String) -> Boolean, - afterDownloadFilter: (Path) -> Boolean, - onStart: () -> Unit, - onSuccess: () -> Unit, - onThrowable: (Throwable) -> Unit, - onFinished: () -> Unit - ) = object : Task.Backgroundable(project, "Downloading files from GitHub") { - - override fun run(indicator: ProgressIndicator) { - onStart() - indicator.isIndeterminate = true - - val httpClient = OkHttpClient.Builder() - .applyIntelliJProxySettings(repositoryUrl) - .build() - - val fileNamesToDownloadUrls = fetchFiles(httpClient, repositoryUrl).filter { preDownloadFilter(it.key) } - indicator.isIndeterminate = false - - var index = 1 - var errors = 0 - fileNamesToDownloadUrls.forEach { fileName, downloadUrl -> - if (indicator.isCanceled) { - return@forEach - } - - indicator.text = "Downloading $fileName" - indicator.fraction = index.toDouble() / fileNamesToDownloadUrls.size - - val targetPath = destinationPath.resolve(fileName) - val success = downloadFile(httpClient, downloadUrl, targetPath) - if (!success) { - errors++ - } - else { - indicator.text = "Analyzing $fileName" - val keepFile = afterDownloadFilter(targetPath) - if (!keepFile) { - try { - Files.deleteIfExists(targetPath) - } - catch (e: Exception) { - log.warn("Failed to delete file: $targetPath", e) - } - } - } - index++ - } - if (errors > 0) { - throw Exception("Failed to download $errors of ${fileNamesToDownloadUrls.size} files") - } - indicator.text = "All files downloaded" - } - - override fun onThrowable(error: Throwable) { - onThrowable(error) - } - - override fun onFinished() { - onFinished() - } - - override fun onSuccess() { - onSuccess() - } - } - - private fun fetchFiles(httpClient: OkHttpClient, repositoryUrl: String): Map { - val apiUrl = repositoryUrl.replace("https://github.com/", "https://api.github.com/repos/") + "/contents" - val request = Request.Builder() - .get() - .url(apiUrl) - .header("Accept", "application/vnd.github.v3+json") - .build() - - val response = httpClient.newCall(request).execute() - if (response.code == 200) { - response.body?.byteStream()?.use { inputStream -> - val rootNode: JsonNode = ObjectMapper().readTree(inputStream) - return rootNode - .filter { it["type"].asText() == "file" } - .associate { it["name"].asText() to it["download_url"].asText() } - } - throw Exception("No HTTP body received from $apiUrl") - } - else { - throw Exception("Failed to fetch file list from $apiUrl: HTTP ${response.code}") - } - } - - private fun downloadFile( - httpClient: OkHttpClient, - downloadUrl: String, - targetPath: Path - ): Boolean { - val request = Request.Builder() - .get() - .url(downloadUrl) - .build() - - log.info("Downloading $downloadUrl to $targetPath") - - val response = httpClient.newCall(request).execute() - return if (response.code == 200) { - response.body?.byteStream()?.use { inputStream -> - Files.createDirectories(targetPath.parent) - Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING) - true - } == true - } - else { - log.warn("Failed to download $downloadUrl to $targetPath: HTTP ${response.code}") - false - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/LocaleContainer.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/LocaleContainer.kt deleted file mode 100644 index 17bb093c..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/LocaleContainer.kt +++ /dev/null @@ -1,30 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common - -import com.intellij.openapi.ui.naturalSorted -import java.util.* - -/** - * A wrapper class for a [Locale]. This is required because the search function - * of the `comboBox` in the UI DSL does work on the human-readable the display - * name. It will use the `toString()` (e.g., `de_DE`) representation of the [Locale] - * instead. - */ -data class LocaleContainer(val locale: Locale) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun toString(): String = locale.displayName - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - val ALL_AVAILABLE_LOCALES = Locale.getAvailableLocales() - .filter { it.displayName.isNotBlank() } - .map { LocaleContainer(it) } - .naturalSorted() - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/NotBlankInputValidator.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/NotBlankInputValidator.kt deleted file mode 100644 index f78f9fa8..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/NotBlankInputValidator.kt +++ /dev/null @@ -1,19 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common - -import com.intellij.openapi.ui.InputValidator - -class NotBlankInputValidator : InputValidator { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun checkInput(inputString: String?): Boolean = - inputString?.isNotBlank() ?: false - - override fun canClose(inputString: String?): Boolean = - inputString?.isNotBlank() ?: false - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/UiExtensions.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/UiExtensions.kt deleted file mode 100644 index de406025..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/UiExtensions.kt +++ /dev/null @@ -1,282 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common - -import com.intellij.lang.Language -import com.intellij.openapi.actionSystem.ActionGroup -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.ex.EditorEx -import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory -import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory -import com.intellij.openapi.observable.properties.ObservableMutableProperty -import com.intellij.openapi.observable.util.transform -import com.intellij.openapi.ui.ValidationInfo -import com.intellij.ui.HyperlinkLabel -import com.intellij.ui.JBColor -import com.intellij.ui.UIBundle -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBRadioButton -import com.intellij.ui.components.JBTextArea -import com.intellij.ui.components.JBTextField -import com.intellij.ui.dsl.builder.Cell -import com.intellij.ui.dsl.builder.Row -import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import com.intellij.ui.tabs.TabInfo -import com.intellij.util.ui.JBEmptyBorder -import com.intellij.util.ui.JBFont -import com.intellij.util.ui.components.BorderLayoutPanel -import java.awt.Color -import java.awt.Font -import java.awt.event.InputEvent -import java.awt.event.ItemEvent -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import java.math.BigDecimal -import java.math.MathContext -import javax.swing.JComponent -import javax.swing.JLabel -import javax.swing.JTable -import javax.swing.JTextField -import javax.swing.ToolTipManager -import javax.swing.border.CompoundBorder - -// -- Properties ---------------------------------------------------------------------------------------------------- // - -val longMaxValue = BigDecimal(Long.MAX_VALUE) -val longMinValue = BigDecimal(Long.MIN_VALUE) - -// -- Exposed Methods ----------------------------------------------------------------------------------------------- // - -/** - * The UI DSL only verifies the range of an `intTextField` on a user input. - */ -@Suppress("UnstableApiUsage") -fun Cell.validateLongValue(range: LongRange? = null) = this.apply { - validationInfo { - if (this@validateLongValue.component.isEnabled) { - val value = this@validateLongValue.component.text.toLongOrNull() - when { - value == null -> error(UIBundle.message("please.enter.a.number")) - range != null && value !in range -> error(UIBundle.message("please.enter.a.number.from.0.to.1", range.first, range.last)) - else -> null - } - } - else { - null - } - } -} - -@Suppress("UnstableApiUsage") -fun Cell.validateBigDecimalValue( - minInclusive: BigDecimal? = null, - mathContext: MathContext = MathContext.UNLIMITED, - toBigDecimal: (String) -> BigDecimal? = { it.toBigDecimalOrNull(mathContext) } -) = this.apply { - validationInfo { - if (!this@validateBigDecimalValue.component.isEnabled) { - return@validationInfo null - } - val value: BigDecimal? = try { - toBigDecimal(this@validateBigDecimalValue.component.text) - } catch (_: Exception) { - return@validationInfo error("Please enter a valid number") - } - when { - value == null -> error("Please enter a number") - minInclusive != null && value < minInclusive -> error("Enter a number greater than or equal to 0") - else -> null - } - } -} - -fun Cell.bind(property: ObservableMutableProperty, value: T) = this.apply { - this.applyToComponent { - isSelected = property.get() == value - - this.addItemListener { event -> - if (event.stateChange == ItemEvent.SELECTED) { - property.set(value) - } - } - } -} - -/** - * IntelliJ's `bindIntText` will silently fail if the input is empty and will - * not execute any other validators. - */ -@Suppress("UnstableApiUsage") -fun Cell.bindIntTextImproved(property: ObservableMutableProperty) = this.apply { - applyToComponent { - text = property.get().toString() - property.afterChange { text = it.toString() } - } - this.whenTextChangedFromUi { - it.toIntOrNull()?.let { intValue -> property.set(intValue) } - } -} - -/** - * IntelliJ's `bindIntText` will silently fail if the input is empty and will - * not execute any other validators. - */ -@Suppress("UnstableApiUsage") -fun Cell.bindLongTextImproved(property: ObservableMutableProperty) = this.apply { - applyToComponent { - text = property.get().toString() - property.afterChange { text = it.toString() } - } - this.whenTextChangedFromUi { - it.toLongOrNull()?.let { longValue -> property.set(longValue) } - } -} - -/** - * IntelliJ's `bindIntText` will silently fail if the input is empty and will - * not execute any other validators. - */ -@Suppress("UnstableApiUsage") -fun Cell.bindDoubleTextImproved(property: ObservableMutableProperty) = this.apply { - applyToComponent { - text = property.get().toString() - property.afterChange { text = it.toString() } - } - this.whenTextChangedFromUi { - it.toDoubleOrNull()?.let { doubleValue -> property.set(doubleValue) } - } -} - -@Suppress("UnstableApiUsage") -fun Cell.validateNonEmpty(errorMessage: String) = this.applyToComponent { - validationInfo { - if (it.text.isEmpty()) { - ValidationInfo(errorMessage) - } - else { - null - } - } -} - -fun Cell.setValidationResultBorder() = this.applyToComponent { - border = CompoundBorder(ValidationResultBorder(this), JBEmptyBorder(3, 5, 3, 5)) -} - -@Suppress("UnstableApiUsage") -fun Cell.validateMinMaxValueRelation(side: ValidateMinIntValueSide, getOppositeValue: () -> Int): Cell = - this.validationInfo { - if (this.component.isEnabled) { - it.text?.toIntOrNull()?.let { thisValue -> - when { - side == ValidateMinIntValueSide.MIN && thisValue > getOppositeValue() -> { - ValidationInfo("Minimum must be smaller than or equal to maximum") - } - - side == ValidateMinIntValueSide.MAX && thisValue < getOppositeValue() -> - ValidationInfo("Maximum must be larger than or equal to minimum") - - else -> null - } - } - } - else { - null - } - } - -fun JBLabel.copyable() = this.apply { setCopyable(true) } - -fun JComponent.wrapWithToolBar(actionEventPlace: String, actions: ActionGroup, toolBarPlace: ToolBarPlace): JComponent { - return BorderLayoutPanel().apply { - val actionToolbar = ActionManager.getInstance().createActionToolbar(actionEventPlace, actions, toolBarPlace.horizontal) - actionToolbar.targetComponent = this@wrapWithToolBar - - when (toolBarPlace) { - ToolBarPlace.LEFT -> { - addToLeft(actionToolbar.component) - addToCenter(this@wrapWithToolBar) - } - - ToolBarPlace.RIGHT, ToolBarPlace.APPEND -> { - addToCenter(this@wrapWithToolBar) - addToRight(actionToolbar.component) - } - } - } -} - -fun JTable.setContextMenu(place: String, actionGroup: ActionGroup) { - val mouseAdapter = object : MouseAdapter() { - - override fun mousePressed(e: MouseEvent) { - handleMouseEvent(e) - } - - override fun mouseReleased(e: MouseEvent) { - handleMouseEvent(e) - } - - private fun handleMouseEvent(e: InputEvent) { - if (e is MouseEvent && e.isPopupTrigger) { - ActionManager.getInstance() - .createActionPopupMenu(place, actionGroup).component - .show(e.getComponent(), e.x, e.y) - } - } - } - addMouseListener(mouseAdapter) -} - -fun Cell.changeFont(scale: Float = 1.0f, style: Int = Font.PLAIN) = this.applyToComponent { - font = JBFont.create(this.font.deriveFont(style), false).biggerOn(scale) -} - -fun Cell.monospaceFont(scale: Float = 1.0f, style: Int = Font.PLAIN) = this.applyToComponent { - font = JBFont.create(Font(Font.MONOSPACED, style, this.font.size)).biggerOn(scale) -} - -fun EditorEx.setLanguage(language: Language) { - val syntaxHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(language, project, null) - highlighter = EditorHighlighterFactory.getInstance().createEditorHighlighter(syntaxHighlighter, EditorColorsManager.getInstance().globalScheme) -} - -fun Row.hyperLink(title: String, url: String) { - cell(HyperlinkLabel(title).apply { - setHyperlinkTarget(url) - }) -} - -fun Color.toJBColor() = this as? JBColor ?: JBColor(this, this) - -@Suppress("UNCHECKED_CAST") -fun TabInfo.castedObject(): T = this.`object` as T - -operator fun ObservableMutableProperty.not(): ObservableMutableProperty = - transform({ !it }) { !it } - -fun BigDecimal.isWithinLongRange(): Boolean = (this <= longMaxValue) && (this >= longMinValue) - -fun Cell.registerDynamicToolTip(toolTipText: () -> String?) { - this.component.toolTipText = null - ToolTipManager.sharedInstance().registerComponent(this.component) - - this.component.addMouseListener(object : MouseAdapter() { - override fun mouseEntered(e: MouseEvent?) { - this@registerDynamicToolTip.component.toolTipText = toolTipText() - } - }) -} - -// -- Private Methods ----------------------------------------------------------------------------------------------- // -// -- Type ---------------------------------------------------------------------------------------------------------- // - -enum class ValidateMinIntValueSide { MIN, MAX } - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -enum class ToolBarPlace(val horizontal: Boolean) { - - LEFT(false), - RIGHT(false), - APPEND(true) -} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/UiUtils.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/UiUtils.kt deleted file mode 100644 index 0eb72fd4..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/common/UiUtils.kt +++ /dev/null @@ -1,161 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.common - -import com.intellij.diff.DiffContentFactory -import com.intellij.diff.DiffManager -import com.intellij.diff.requests.DiffRequest -import com.intellij.diff.requests.SimpleDiffRequest -import com.intellij.openapi.actionSystem.ActionGroup -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.ToggleAction -import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.ui.popup.Balloon -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.ui.popup.util.PopupUtil -import com.intellij.ui.HyperlinkLabel -import com.intellij.ui.JBColor -import com.intellij.ui.ScrollPaneFactory -import com.intellij.ui.popup.PopupState -import com.intellij.util.ui.ColumnInfo -import com.intellij.util.ui.UIUtil -import java.awt.Dimension -import java.awt.event.InputEvent -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent -import javax.swing.Icon -import javax.swing.JComponent - -object UiUtils { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - fun showDiffDialog( - title: String, - firstTitle: String, - secondTitle: String, - firstText: String, - secondText: String - ) { - val diffContentFactory = DiffContentFactory.getInstance() - val firstContent = diffContentFactory.create(firstText) - val secondContent = diffContentFactory.create(secondText) - val request: DiffRequest = SimpleDiffRequest(title, firstContent, secondContent, firstTitle, secondTitle) - DiffManager.getInstance().showDiff(null, request) - } - - fun dumbAwareAction(title: String, icon: Icon? = null, action: (AnActionEvent) -> Unit) = - object : DumbAwareAction(title, null, icon) { - - override fun actionPerformed(e: AnActionEvent) { - action(e) - } - } - - @Suppress("UnstableApiUsage") - fun actionsPopup( - title: String, - icon: Icon? = null, - actions: List - ): ActionGroup = object : ActionGroup(title, null, icon), DumbAware { - - private val popupState = PopupState.forPopup() - - init { - isPopup = true - templatePresentation.isPerformGroup = actions.isNotEmpty() - } - - override fun getChildren(e: AnActionEvent?): Array = actions.toTypedArray() - - override fun actionPerformed(e: AnActionEvent) { - if (popupState.isRecentlyHidden) { - return - } - - val popup = JBPopupFactory - .getInstance() - .createActionGroupPopup( - null, - this, - e.dataContext, - JBPopupFactory.ActionSelectionAid.MNEMONICS, - true - ) - popupState.prepareToShow(popup) - PopupUtil.showForActionButtonEvent(popup, e) - } - } - - fun createLink(title: String, url: String): HyperlinkLabel { - return HyperlinkLabel(title).apply { - setHyperlinkTarget(url) - } - } - - fun createToggleAction( - title: String, - isSelected: () -> Boolean, - setSelected: (Boolean) -> Unit - ) = object : ToggleAction(title) { - override fun isSelected(e: AnActionEvent): Boolean = isSelected() - - override fun setSelected(e: AnActionEvent, state: Boolean) { - setSelected(state) - } - - override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT - } - - fun createContextMenuMouseListener( - place: String, - actionGroup: (MouseEvent) -> ActionGroup? - ) = object : MouseAdapter() { - override fun mousePressed(e: MouseEvent) { - handleMouseEvent(e) - } - - override fun mouseReleased(e: MouseEvent) { - handleMouseEvent(e) - } - - private fun handleMouseEvent(e: InputEvent) { - if (e is MouseEvent && e.isPopupTrigger) { - actionGroup(e)?.let { - ActionManager.getInstance() - .createActionPopupMenu(place, it).component - .show(e.component, e.x, e.y) - } - } - } - } - - fun simpleColumnInfo(name: String, displayValue: (T) -> String, sortValue: (T) -> Comparable<*>) = - object : ColumnInfo(name) { - - override fun valueOf(item: T): String = displayValue(item) - - override fun getComparator(): Comparator = compareBy { sortValue(it) } - } - - fun createPopup(content: JComponent, width: Int = 600, height: Int = 450): Balloon = - JBPopupFactory.getInstance() - .createBalloonBuilder(ScrollPaneFactory.createScrollPane(content, true).apply { - preferredSize = Dimension(width, height) - }) - .setDialogMode(true) - .setFillColor(UIUtil.getPanelBackground()) - .setBorderColor(JBColor.border()) - .setBlockClicksThroughBalloon(true) - .setRequestFocus(true) - .createBalloon() - .apply { - setAnimationEnabled(false) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsApplicationSettings.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsApplicationSettings.kt deleted file mode 100644 index 02de7f37..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsApplicationSettings.kt +++ /dev/null @@ -1,180 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings - -import com.intellij.CommonBundle -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.PersistentStateComponent -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.SettingsCategory -import com.intellij.openapi.components.State -import com.intellij.openapi.components.Storage -import com.intellij.openapi.diagnostic.logger -import com.intellij.util.xmlb.annotations.Attribute -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings.ApplicationState -import java.security.Provider -import java.security.Security - -@Service -@State( - name = "DeveloperToolsApplicationSettingsV1", - storages = [Storage("developer-tools.xml")], - category = SettingsCategory.TOOLS -) -internal class DeveloperToolsApplicationSettings : PersistentStateComponent { - // -- Properties -------------------------------------------------------------------------------------------------- // - - var addOpenMainDialogActionToMainToolbar: Boolean by ValueProperty(ADD_OPEN_MAIN_DIALOG_ACTION_TO_MAIN_TOOLBAR_DEFAULT) - var promoteAddOpenMainDialogActionToMainToolbar: Boolean by ValueProperty(PROMOTE_ADD_OPEN_MAIN_DIALOG_ACTION_TO_MAIN_TOOLBAR) - var loadExamples: Boolean by ValueProperty(LOAD_EXAMPLES_DEFAULT) - var saveConfigurations: Boolean by ValueProperty(SAVE_CONFIGURATIONS_DEFAULT) - var saveInputs: Boolean by ValueProperty(SAVE_INPUTS_DEFAULT) - var saveSensitiveInputs: Boolean by ValueProperty(SAVE_SENSITIVE_INPUTS_DEFAULT) - var editorSoftWraps: Boolean by ValueProperty(EDITOR_SOFT_WRAPS_DEFAULT) - var editorShowSpecialCharacters: Boolean by ValueProperty(EDITOR_SHOW_SPECIAL_CHARACTERS_DEFAULT) - var editorShowWhitespaces: Boolean by ValueProperty(EDITOR_SHOW_WHITESPACES_DEFAULT) - var toolsMenuTreeShowGroupNodes: Boolean by ValueProperty(TOOLS_MENU_TREE_GROUP_NODES_DEFAULT) - var toolsMenuTreeOrderAlphabetically: Boolean by ValueProperty(TOOLS_MENU_TREE_ORDER_ALPHABETICALLY_DEFAULT) - var autoDetectActionHandlingInstance: Boolean by ValueProperty(AUTO_DETECT_ACTION_HANDLING_INSTANCE_DEFAULT) - var selectedActionHandlingInstance: ActionHandlingInstance by ValueProperty(SELECTED_ACTION_HANDLING_INSTANCE_DEFAULT) - var showInternalTools: Boolean by ValueProperty(SHOW_INTERNAL_TOOLS_DEFAULT) - var hideWorkbenchTabsOnSingleTab: Boolean by ValueProperty(HIDE_WORKBENCH_TABS_ON_SINGLE_TAB) - - var modificationCounter = 0 - private set - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - try { - val bouncyCastleProviderClass = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider") - val bouncyCastleProvider = bouncyCastleProviderClass.getConstructor().newInstance() - Security.addProvider(bouncyCastleProvider as Provider) - } catch (e: Exception) { - log.debug("Can't load BouncyCastleProvider", e) - } - } - - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - fun update(applyModifications: (DeveloperToolsApplicationSettings) -> Unit) { - applyModifications(this) - modificationCounter++ - } - - override fun getState(): ApplicationState = ApplicationState( - addOpenMainDialogActionToMainToolbar = addOpenMainDialogActionToMainToolbar, - promoteAddOpenMainDialogActionToMainToolbar = promoteAddOpenMainDialogActionToMainToolbar, - loadExamples = loadExamples, - saveConfigurations = saveConfigurations, - saveInputs = saveInputs, - saveSensitiveInputs = saveSensitiveInputs, - editorSoftWraps = editorSoftWraps, - editorShowSpecialCharacters = editorShowSpecialCharacters, - editorShowWhitespaces = editorShowWhitespaces, - toolsMenuTreeOrderAlphabetically = toolsMenuTreeOrderAlphabetically, - toolsMenuTreeShowGroupNodes = toolsMenuTreeShowGroupNodes, - autoDetectActionHandlingInstance = autoDetectActionHandlingInstance, - selectedActionHandlingInstance = selectedActionHandlingInstance, - showInternalTools = showInternalTools, - hideWorkbenchTabsOnSingleTab = hideWorkbenchTabsOnSingleTab - ) - - override fun loadState(state: ApplicationState) { - addOpenMainDialogActionToMainToolbar = (state.addOpenMainDialogActionToMainToolbar ?: ADD_OPEN_MAIN_DIALOG_ACTION_TO_MAIN_TOOLBAR_DEFAULT) - promoteAddOpenMainDialogActionToMainToolbar = (state.promoteAddOpenMainDialogActionToMainToolbar ?: PROMOTE_ADD_OPEN_MAIN_DIALOG_ACTION_TO_MAIN_TOOLBAR) - loadExamples = (state.loadExamples ?: LOAD_EXAMPLES_DEFAULT) - saveConfigurations = (state.saveConfigurations ?: SAVE_CONFIGURATIONS_DEFAULT) - saveInputs = (state.saveInputs ?: SAVE_INPUTS_DEFAULT) - saveSensitiveInputs = (state.saveSensitiveInputs ?: SAVE_SENSITIVE_INPUTS_DEFAULT) - editorSoftWraps = (state.editorSoftWraps ?: EDITOR_SOFT_WRAPS_DEFAULT) - editorShowSpecialCharacters = (state.editorShowSpecialCharacters ?: EDITOR_SHOW_SPECIAL_CHARACTERS_DEFAULT) - editorShowWhitespaces = (state.editorShowWhitespaces ?: EDITOR_SHOW_WHITESPACES_DEFAULT) - toolsMenuTreeShowGroupNodes = (state.toolsMenuTreeShowGroupNodes ?: TOOLS_MENU_TREE_GROUP_NODES_DEFAULT) - toolsMenuTreeOrderAlphabetically = (state.toolsMenuTreeOrderAlphabetically ?: TOOLS_MENU_TREE_ORDER_ALPHABETICALLY_DEFAULT) - autoDetectActionHandlingInstance = (state.autoDetectActionHandlingInstance ?: AUTO_DETECT_ACTION_HANDLING_INSTANCE_DEFAULT) - selectedActionHandlingInstance = (state.selectedActionHandlingInstance ?: SELECTED_ACTION_HANDLING_INSTANCE_DEFAULT) - showInternalTools = (state.showInternalTools ?: SHOW_INTERNAL_TOOLS_DEFAULT) - hideWorkbenchTabsOnSingleTab = (state.hideWorkbenchTabsOnSingleTab ?: HIDE_WORKBENCH_TABS_ON_SINGLE_TAB) - } - - fun createSensitiveInputsHandlingToolTipText(): String? = - if (saveInputs && !saveSensitiveInputs) { - "This sensitive input field will be cleared after the application is closed.
    " + - "You can deactivate this behavior in the ${CommonBundle.settingsTitle().lowercase()}." - } - else { - null - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - enum class ActionHandlingInstance(private val title: String) { - - TOOL_WINDOW("Tool Window"), - DIALOG("Dialog"); - - override fun toString(): String = title - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - data class ApplicationState( - @get:Attribute("addOpenMainDialogActionToMainToolbar") - var addOpenMainDialogActionToMainToolbar: Boolean? = null, - @get:Attribute("promoteAddOpenMainDialogActionToMainToolbar") - var promoteAddOpenMainDialogActionToMainToolbar: Boolean? = null, - @get:Attribute("loadExamples") - var loadExamples: Boolean? = null, - @get:Attribute("saveConfigurations") - var saveConfigurations: Boolean? = null, - @get:Attribute("saveInputs") - var saveInputs: Boolean? = null, - @get:Attribute("saveSensitiveInputs") - var saveSensitiveInputs: Boolean? = null, - @get:Attribute("editorSoftWraps") - var editorSoftWraps: Boolean? = null, - @get:Attribute("editorShowSpecialCharacters") - var editorShowSpecialCharacters: Boolean? = null, - @get:Attribute("editorShowWhitespaces") - var editorShowWhitespaces: Boolean? = null, - @get:Attribute("toolsMenuTreeShowGroupNodes") - var toolsMenuTreeShowGroupNodes: Boolean? = null, - @get:Attribute("toolsMenuTreeOrderAlphabetically") - var toolsMenuTreeOrderAlphabetically: Boolean? = null, - @get:Attribute("autoDetectActionHandlingInstance") - var autoDetectActionHandlingInstance: Boolean? = null, - @get:Attribute("selectedActionHandlingInstance") - var selectedActionHandlingInstance: ActionHandlingInstance? = null, - @get:Attribute("showInternalTools") - var showInternalTools: Boolean? = null, - @get:Attribute("hideWorkbenchTabsOnSingleTab") - var hideWorkbenchTabsOnSingleTab: Boolean? = null - ) - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val log = logger() - - val instance: DeveloperToolsApplicationSettings - get() = ApplicationManager.getApplication().getService(DeveloperToolsApplicationSettings::class.java) - - const val ADD_OPEN_MAIN_DIALOG_ACTION_TO_MAIN_TOOLBAR_DEFAULT = false - const val PROMOTE_ADD_OPEN_MAIN_DIALOG_ACTION_TO_MAIN_TOOLBAR = true - const val LOAD_EXAMPLES_DEFAULT = true - const val SAVE_INPUTS_DEFAULT = true - const val SAVE_SENSITIVE_INPUTS_DEFAULT = false - const val SAVE_CONFIGURATIONS_DEFAULT = true - const val EDITOR_SOFT_WRAPS_DEFAULT = true - const val EDITOR_SHOW_SPECIAL_CHARACTERS_DEFAULT = false - const val EDITOR_SHOW_WHITESPACES_DEFAULT = false - const val TOOLS_MENU_TREE_GROUP_NODES_DEFAULT = false - const val TOOLS_MENU_TREE_ORDER_ALPHABETICALLY_DEFAULT = true - const val AUTO_DETECT_ACTION_HANDLING_INSTANCE_DEFAULT = true - const val SHOW_INTERNAL_TOOLS_DEFAULT = false - val SELECTED_ACTION_HANDLING_INSTANCE_DEFAULT = ActionHandlingInstance.TOOL_WINDOW - const val HIDE_WORKBENCH_TABS_ON_SINGLE_TAB = true - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsConfigurable.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsConfigurable.kt deleted file mode 100644 index 9f59d728..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsConfigurable.kt +++ /dev/null @@ -1,209 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings - -import com.intellij.openapi.options.Configurable -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.panel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.not -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings.ActionHandlingInstance -import javax.swing.JComponent - -class DeveloperToolsConfigurable : Configurable { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private lateinit var addOpenMainDialogActionToMainToolbar: ValueProperty - private lateinit var saveConfigurations: ValueProperty - private lateinit var saveInputs: ValueProperty - private lateinit var saveSensitiveInputs: ValueProperty - private lateinit var loadExamples: ValueProperty - private lateinit var dialogIsModal: ValueProperty - private lateinit var editorSoftWraps: ValueProperty - private lateinit var editorShowSpecialCharacters: ValueProperty - private lateinit var editorShowWhitespaces: ValueProperty - private lateinit var toolsMenuShowGroupNodes: ValueProperty - private lateinit var toolsMenuOrderAlphabetically: ValueProperty - private lateinit var autoDetectActionHandlingInstance: ValueProperty - private lateinit var selectedActionHandlingInstance: ValueProperty - private lateinit var showInternalTools: ValueProperty - private lateinit var hideWorkbenchTabsOnSingleTab: ValueProperty - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun getDisplayName(): String = "Developer Tools" - - override fun createComponent(): JComponent = panel { - val developerToolsApplicationSettings = DeveloperToolsApplicationSettings.instance - - row { - addOpenMainDialogActionToMainToolbar = ValueProperty(developerToolsApplicationSettings.addOpenMainDialogActionToMainToolbar) - checkBox("Add 'Developer Tools' action to the main toolbar during startup") - .comment("If the action was removed manually in the past, this automatic mechanism will not work. The 'Developer Tools' action must first be manually added to the 'Main Toolbar Right' again (or 'Main Toolbar' in the old UI).") - .bindSelected(addOpenMainDialogActionToMainToolbar) - } - - row { - saveConfigurations = ValueProperty(developerToolsApplicationSettings.saveConfigurations) - checkBox("Remember configurations") - .bindSelected(saveConfigurations) - } - row { - saveInputs = ValueProperty(developerToolsApplicationSettings.saveInputs) - checkBox("Remember inputs") - .bindSelected(saveInputs) - } - row { - saveSensitiveInputs = ValueProperty(developerToolsApplicationSettings.saveSensitiveInputs) - checkBox("Remember sensitive inputs") - .bindSelected(saveSensitiveInputs) - }.comment("Sensitive inputs will be stored in plaintext.") - row { - loadExamples = ValueProperty(developerToolsApplicationSettings.loadExamples) - checkBox("Load examples") - .bindSelected(loadExamples) - }.bottomGap(BottomGap.SMALL) - row { - hideWorkbenchTabsOnSingleTab = ValueProperty(developerToolsApplicationSettings.hideWorkbenchTabsOnSingleTab) - checkBox("Hide workbench tabs if there is only one tab") - .bindSelected(hideWorkbenchTabsOnSingleTab) - }.bottomGap(BottomGap.SMALL) - - buttonsGroup("External action handling") { - row("Open tools in:") { - autoDetectActionHandlingInstance = ValueProperty(developerToolsApplicationSettings.autoDetectActionHandlingInstance) - radioButton("Auto detect") - .bindSelected(autoDetectActionHandlingInstance) - - radioButton("Use:") - .bindSelected(autoDetectActionHandlingInstance.not()) - .gap(RightGap.SMALL) - selectedActionHandlingInstance = ValueProperty(developerToolsApplicationSettings.selectedActionHandlingInstance) - comboBox(ActionHandlingInstance.entries) - .bindItem(selectedActionHandlingInstance) - .enabledIf(autoDetectActionHandlingInstance.not()) - }.bottomGap(BottomGap.NONE) - row { - comment("The plugin provides actions in other places in IntelliJ (e.g. in the editor or the project file tree) that open a developer tool. With this setting you can specify whether these actions open the dialog or the tool window.") - } - } - - groupRowsRange("Default Editor Settings") { - row { - editorSoftWraps = ValueProperty(developerToolsApplicationSettings.editorSoftWraps) - checkBox("Soft-wrap") - .bindSelected(editorSoftWraps) - } - row { - editorShowSpecialCharacters = ValueProperty(developerToolsApplicationSettings.editorShowSpecialCharacters) - checkBox("Show special characters") - .bindSelected(editorShowSpecialCharacters) - } - row { - editorShowWhitespaces = ValueProperty(developerToolsApplicationSettings.editorShowWhitespaces) - checkBox("Show whitespaces") - .bindSelected(editorShowWhitespaces) - } - } - - groupRowsRange("Advanced") { - row { - dialogIsModal = ValueProperty(DeveloperToolsDialogSettings.instance.dialogIsModal) - checkBox("Dialog is modal and must be closed before continuing to work with IntelliJ") - .bindSelected(dialogIsModal) - } - row { - toolsMenuShowGroupNodes = ValueProperty(developerToolsApplicationSettings.toolsMenuTreeShowGroupNodes) - checkBox("Group tools in the menu") - .bindSelected(toolsMenuShowGroupNodes) - } - row { - toolsMenuOrderAlphabetically = ValueProperty(developerToolsApplicationSettings.toolsMenuTreeOrderAlphabetically) - checkBox("Order tools in the menu alphabetically") - .bindSelected(toolsMenuOrderAlphabetically) - } - row { - showInternalTools = ValueProperty(developerToolsApplicationSettings.showInternalTools) - checkBox("Show internal tools") - .bindSelected(showInternalTools) - .comment("Enables additional developer tools that are only useful for special applications, such as supporting IntelliJ plugin development.") - } - } - - row { - cell( - UiUtils.createLink( - title = "Make a feature request or report an issue", - url = "https://github.com/marcelkliemannel/intellij-developer-tools-plugin/issues" - ) - ) - }.topGap(TopGap.MEDIUM) - } - - override fun isModified(): Boolean { - val developerToolsApplicationSettings = DeveloperToolsApplicationSettings.instance - return developerToolsApplicationSettings.addOpenMainDialogActionToMainToolbar != addOpenMainDialogActionToMainToolbar.get() || - developerToolsApplicationSettings.saveConfigurations != saveConfigurations.get() || - developerToolsApplicationSettings.saveInputs != saveInputs.get() || - developerToolsApplicationSettings.saveSensitiveInputs != saveSensitiveInputs.get() || - developerToolsApplicationSettings.loadExamples != loadExamples.get() || - DeveloperToolsDialogSettings.instance.dialogIsModal != dialogIsModal.get() || - developerToolsApplicationSettings.editorSoftWraps != editorSoftWraps.get() || - developerToolsApplicationSettings.editorShowWhitespaces != editorShowWhitespaces.get() || - developerToolsApplicationSettings.editorShowSpecialCharacters != editorShowSpecialCharacters.get() || - developerToolsApplicationSettings.toolsMenuTreeShowGroupNodes != toolsMenuShowGroupNodes.get() || - developerToolsApplicationSettings.toolsMenuTreeOrderAlphabetically != toolsMenuOrderAlphabetically.get() || - developerToolsApplicationSettings.autoDetectActionHandlingInstance != autoDetectActionHandlingInstance.get() || - developerToolsApplicationSettings.selectedActionHandlingInstance != selectedActionHandlingInstance.get() || - developerToolsApplicationSettings.showInternalTools != showInternalTools.get() || - developerToolsApplicationSettings.hideWorkbenchTabsOnSingleTab != hideWorkbenchTabsOnSingleTab.get() - } - - override fun apply() { - DeveloperToolsApplicationSettings.instance.update { - it.addOpenMainDialogActionToMainToolbar = addOpenMainDialogActionToMainToolbar.get() - it.saveConfigurations = saveConfigurations.get() - it.saveInputs = saveInputs.get() - it.saveSensitiveInputs = saveSensitiveInputs.get() - it.loadExamples = loadExamples.get() - it.editorSoftWraps = editorSoftWraps.get() - it.editorShowWhitespaces = editorShowWhitespaces.get() - it.editorShowSpecialCharacters = editorShowSpecialCharacters.get() - it.toolsMenuTreeShowGroupNodes = toolsMenuShowGroupNodes.get() - it.toolsMenuTreeOrderAlphabetically = toolsMenuOrderAlphabetically.get() - it.autoDetectActionHandlingInstance = autoDetectActionHandlingInstance.get() - it.selectedActionHandlingInstance = selectedActionHandlingInstance.get() - it.showInternalTools = showInternalTools.get() - it.hideWorkbenchTabsOnSingleTab = hideWorkbenchTabsOnSingleTab.get() - } - DeveloperToolsDialogSettings.instance.dialogIsModal = dialogIsModal.get() - } - - override fun reset() { - val developerToolsApplicationSettings = DeveloperToolsApplicationSettings.instance - addOpenMainDialogActionToMainToolbar.set(developerToolsApplicationSettings.addOpenMainDialogActionToMainToolbar) - saveConfigurations.set(developerToolsApplicationSettings.saveConfigurations) - saveInputs.set(developerToolsApplicationSettings.saveInputs) - saveSensitiveInputs.set(developerToolsApplicationSettings.saveSensitiveInputs) - loadExamples.set(developerToolsApplicationSettings.loadExamples) - dialogIsModal.set(DeveloperToolsDialogSettings.instance.dialogIsModal) - editorSoftWraps.set(developerToolsApplicationSettings.editorSoftWraps) - editorShowWhitespaces.set(developerToolsApplicationSettings.editorShowWhitespaces) - editorShowSpecialCharacters.set(developerToolsApplicationSettings.editorShowSpecialCharacters) - toolsMenuShowGroupNodes.set(developerToolsApplicationSettings.toolsMenuTreeShowGroupNodes) - toolsMenuOrderAlphabetically.set(developerToolsApplicationSettings.toolsMenuTreeOrderAlphabetically) - autoDetectActionHandlingInstance.set(developerToolsApplicationSettings.autoDetectActionHandlingInstance) - selectedActionHandlingInstance.set(developerToolsApplicationSettings.selectedActionHandlingInstance) - showInternalTools.set(developerToolsApplicationSettings.showInternalTools) - hideWorkbenchTabsOnSingleTab.set(developerToolsApplicationSettings.hideWorkbenchTabsOnSingleTab) - apply() - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsDialogSettings.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsDialogSettings.kt deleted file mode 100644 index 7ba4d260..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsDialogSettings.kt +++ /dev/null @@ -1,70 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.PersistentStateComponent -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.SettingsCategory -import com.intellij.openapi.components.State -import com.intellij.openapi.components.Storage -import com.intellij.util.xmlb.annotations.Attribute -import com.intellij.util.xmlb.annotations.Property -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsDialogSettings.DialogState - -@Service -@State( - name = "DeveloperToolsDialogSettingsV1", - storages = [Storage("developer-tools.xml")], - category = SettingsCategory.TOOLS -) -internal class DeveloperToolsDialogSettings : - DeveloperToolsInstanceSettings(), - PersistentStateComponent { - // -- Properties -------------------------------------------------------------------------------------------------- // - - var dialogIsModal: Boolean by ValueProperty(DIALOG_IS_MODAL_DEFAULT) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun getState(): DialogState { - val instanceState = super.getState() - return DialogState( - dialogIsModal = dialogIsModal, - developerToolsConfigurations = instanceState.developerToolsConfigurations, - lastSelectedContentNodeId = instanceState.lastSelectedContentNodeId, - expandedGroupNodeIds = instanceState.expandedGroupNodeIds - ) - } - - override fun loadState(state: DialogState) { - super.loadState(state) - dialogIsModal = (state.dialogIsModal ?: DIALOG_IS_MODAL_DEFAULT) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - @Property(assertIfNoBindings = false) - class DialogState( - @get:Attribute("dialogIsModal") - var dialogIsModal: Boolean? = null, - developerToolsConfigurations: List? = null, - lastSelectedContentNodeId: String? = null, - expandedGroupNodeIds: List? = null - ) : InstanceState( - developerToolsConfigurations = developerToolsConfigurations, - lastSelectedContentNodeId = lastSelectedContentNodeId, - expandedGroupNodeIds = expandedGroupNodeIds - ) - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - val instance: DeveloperToolsDialogSettings - get() = ApplicationManager.getApplication().getService(DeveloperToolsDialogSettings::class.java) - - const val DIALOG_IS_MODAL_DEFAULT = false - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsInstanceSettings.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsInstanceSettings.kt deleted file mode 100644 index 231aca72..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsInstanceSettings.kt +++ /dev/null @@ -1,292 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings - -import com.intellij.openapi.diagnostic.logger -import com.intellij.ui.JBColor -import com.intellij.util.xmlb.Converter -import com.intellij.util.xmlb.annotations.Attribute -import com.intellij.util.xmlb.annotations.Tag -import com.intellij.util.xmlb.annotations.XCollection -import com.intellij.util.xmlb.annotations.XCollection.Style.v2 -import com.jetbrains.rd.util.UUID -import com.jetbrains.rd.util.firstOrNull -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PersistentProperty -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.SENSITIVE -import java.math.BigDecimal -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList -import kotlin.reflect.KClass -import kotlin.reflect.cast - -internal abstract class DeveloperToolsInstanceSettings { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val developerToolsConfigurations = ConcurrentHashMap>() - - val lastSelectedContentNodeId: ValueProperty = ValueProperty(null) - var expandedGroupNodeIds: MutableSet? = null - private set - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - fun setExpandedGroupNodeIds(expandedGroupNodeIds: Set) { - this.expandedGroupNodeIds = expandedGroupNodeIds.toMutableSet() - } - - fun getDeveloperToolConfigurations(developerToolId: String): List = - developerToolsConfigurations[developerToolId]?.toList() ?: emptyList() - - fun createDeveloperToolConfiguration(developerToolId: String): DeveloperToolConfiguration { - val newDeveloperToolConfiguration = DeveloperToolConfiguration( - name = "Workbench", - id = UUID.randomUUID(), - persistentProperties = emptyMap() - ) - developerToolsConfigurations.compute(developerToolId) { _, developerToolConfigurations -> - (developerToolConfigurations ?: CopyOnWriteArrayList()).also { it.add(newDeveloperToolConfiguration) } - } - return newDeveloperToolConfiguration - } - - fun removeDeveloperToolConfiguration(developerToolId: String, developerToolConfiguration: DeveloperToolConfiguration) { - developerToolsConfigurations[developerToolId]?.remove(developerToolConfiguration) - } - - open fun getState(): InstanceState { - val stateDeveloperToolsConfigurations = developerToolsConfigurations.asSequence() - .flatMap { (developerToolId, developerToolConfigurations) -> - developerToolConfigurations.mapNotNull { createDeveloperToolConfigurationState(developerToolId, it) } - }.toList() - - return InstanceState( - developerToolsConfigurations = stateDeveloperToolsConfigurations, - lastSelectedContentNodeId = lastSelectedContentNodeId.get(), - expandedGroupNodeIds = expandedGroupNodeIds?.toList() - ) - } - - open fun loadState(state: InstanceState) { - lastSelectedContentNodeId.set(state.lastSelectedContentNodeId) - setExpandedGroupNodeIds(state.expandedGroupNodeIds?.toSet() ?: emptySet()) - - developerToolsConfigurations.clear() - state.developerToolsConfigurations - ?.filter { it.developerToolId != null && it.id != null && it.name != null && it.properties != null } - ?.forEach { developerToolsConfigurationState -> - val developerToolConfiguration = DeveloperToolConfiguration( - name = developerToolsConfigurationState.name!!, - id = UUID.fromString(developerToolsConfigurationState.id!!), - persistentProperties = developerToolsConfigurationState.properties - ?.asSequence() - // The `type` property can be `null` in cases in which a type was - // removed from the `PropertyType` enum. - ?.filter { it.key != null && it.type != null && it.value != null } - ?.filter { shouldSavePropertyType(it.type) } - ?.map { it.key!! to PersistentProperty(it.key!!, it.value!!, it.type!!) } - ?.toMap() ?: emptyMap() - ) - - developerToolsConfigurations.compute(developerToolsConfigurationState.developerToolId!!) { _, developerToolConfigurations -> - (developerToolConfigurations ?: CopyOnWriteArrayList()).also { it.add(developerToolConfiguration) } - } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun shouldSavePropertyType(propertyType: PropertyType?): Boolean = - when (propertyType) { - CONFIGURATION -> DeveloperToolsApplicationSettings.instance.saveConfigurations - INPUT -> DeveloperToolsApplicationSettings.instance.saveInputs - SENSITIVE -> DeveloperToolsApplicationSettings.instance.saveSensitiveInputs - null -> false - } - - private fun createDeveloperToolConfigurationState( - developerToolId: String, - developerToolConfiguration: DeveloperToolConfiguration - ): DeveloperToolConfigurationState? { - val properties = if (developerToolConfiguration.wasConsumedByDeveloperTool) { - developerToolConfiguration.properties - .filter { (_, property) -> property.valueWasChanged() } - .map { PersistentProperty(key = it.key, value = it.value.reference.get(), it.value.type) } - } - else { - developerToolConfiguration.persistentProperties.values - } - if (properties.isEmpty()) { - return null - } - - return DeveloperToolConfigurationState( - developerToolId = developerToolId, - id = developerToolConfiguration.id.toString(), - name = developerToolConfiguration.name, - properties = properties - .filter { shouldSavePropertyType(it.type) } - .map { DeveloperToolConfigurationProperty(key = it.key, value = it.value, type = it.type) } - ) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - open class InstanceState( - @get:XCollection(style = v2, elementName = "developerToolsConfigurations") - var developerToolsConfigurations: List? = null, - @get:Attribute("lastSelectedContentNodeId") - var lastSelectedContentNodeId: String? = null, - @get:XCollection(style = v2, elementName = "expandedGroupNodeId") - var expandedGroupNodeIds: List? = null - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - @Tag(value = "developerToolConfiguration") - data class DeveloperToolConfigurationState( - @get:Attribute("developerToolId") - var developerToolId: String? = null, - @get:Attribute("id") - var id: String? = null, - @get:Attribute("name") - var name: String? = null, - @get:XCollection(style = v2, elementName = "properties") - var properties: List? = null, - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - @Tag(value = "property") - data class DeveloperToolConfigurationProperty( - @get:Attribute("key") - var key: String? = null, - @get:Attribute("value", converter = StatePropertyValueConverter::class) - var value: Any? = null, - @get:Attribute("type", converter = PropertyTypeConverter::class) - var type: PropertyType? = null - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - /** - * Using a dedicated converter to handle removed values from the [PropertyType]. - */ - class PropertyTypeConverter : Converter() { - - override fun fromString(value: String): PropertyType? { - return PropertyType.entries.firstOrNull { it.name == value } - } - - override fun toString(value: PropertyType): String { - return value.name - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class StatePropertyValueConverter : Converter() { - - override fun toString(value: Any): String { - val (serializedValue, valueType) = when (value) { - is Enum<*> -> { - // It's important to use the Java qualified name here because it - // separates subclass names with a `$` and not with a `.` as in - // Kotlin. Otherwise, the deserialization via `Class.forName()` would - // not work. - Pair(value.name, value::class.java.name) - } - - is Boolean, is Int, is Long, is Float, is Double -> value.toString() to value::class.qualifiedName!! - is String -> value to String::class.qualifiedName!! - is JBColor -> value.rgb.toString() to JBColor::class.qualifiedName!! - is LocaleContainer -> value.locale.toLanguageTag() to LocaleContainer::class.qualifiedName!! - is BigDecimal -> value.toString() to BigDecimal::class.qualifiedName!! - else -> error("Unsupported configuration property type: ${value::class.qualifiedName}") - } - return "${valueType}$PROPERTY_TYPE_VALUE_DELIMITER$serializedValue" - } - - /** - * If the value can't be restored, this method must return null. - * All properties with null values will be filtered in - * [DeveloperToolsInstanceSettings.loadState]. - */ - override fun fromString(value: String): Any? { - val valueAndType = value.split(PROPERTY_TYPE_VALUE_DELIMITER, limit = 2) - check(valueAndType.size == 2) { "Malformed serialized value: $value" } - val valueType = applyTypeLegacy(valueAndType[0]) - val actualValue = valueAndType[1] - return when (valueType) { - Boolean::class.qualifiedName -> actualValue.toBoolean() - Int::class.qualifiedName -> actualValue.toInt() - Long::class.qualifiedName -> actualValue.toLong() - Float::class.qualifiedName -> actualValue.toFloat() - Double::class.qualifiedName -> actualValue.toDouble() - String::class.qualifiedName -> actualValue - JBColor::class.qualifiedName -> JBColor(actualValue.toInt(), actualValue.toInt()) - LocaleContainer::class.qualifiedName -> LocaleContainer(Locale.forLanguageTag(actualValue)) - BigDecimal::class.qualifiedName -> BigDecimal(actualValue) - else -> parseValue(valueType, actualValue) - } - } - - private fun applyTypeLegacy(type: String): String { - val legacyToApply = pre320TypePackageLegacy.filter { type.startsWith(it.key) }.firstOrNull() - ?: return type - - return "${legacyToApply.value}${type.substring(legacyToApply.key.length)}" - } - - private fun parseValue(valueType: String, value: String): Any? { - return try { - val valueTypeClass = Class.forName(valueType, false, this::class.java.classLoader) - if (valueTypeClass.isEnum) { - valueTypeClass.enumConstants.first { Enum::class.cast(it).name == value } - } - else { - error("Unsupported configuration property: $valueType") - } - } catch (e: Exception) { - log.warn("Failed to load class '$valueType' of value '$value'", e) - null - } - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val log = logger() - - private val pre320TypePackageLegacy = mapOf( - "dev.turingcomplete.intellijdevelopertoolsplugins._internal.tool." to "dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.", - "dev.turingcomplete.intellijdevelopertoolsplugins._internal.common." to "dev.turingcomplete.intellijdevelopertoolsplugin._internal.common." - ) - private const val PROPERTY_TYPE_VALUE_DELIMITER = "|" - private val SUPPORTED_TYPES = setOf>( - Boolean::class, - Int::class, - Long::class, - Float::class, - Double::class, - String::class, - JBColor::class, - LocaleContainer::class, - BigDecimal::class - ) - - fun assertPersistableType(type: KClass): KClass { - check(type.java.isEnum || SUPPORTED_TYPES.contains(type)) { - "Unsupported configuration/input property type: ${type.qualifiedName}" - } - return type - } - } -} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/DeveloperToolsActionGroup.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/DeveloperToolsActionGroup.kt deleted file mode 100644 index 22ee3dc1..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/editor/action/DeveloperToolsActionGroup.kt +++ /dev/null @@ -1,24 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.action - -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.ui.IconManager - -internal class DeveloperToolsActionGroup : DefaultActionGroup("Developer Tools", true) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - templatePresentation.icon = icon - templatePresentation.isHideGroupIfEmpty = true - } - - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val icon = IconManager.getInstance().getIcon("dev/turingcomplete/intellijdevelopertoolsplugin/icons/action.svg", DeveloperToolsActionGroup::class.java.classLoader) - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CliCommandConverter.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CliCommandConverter.kt deleted file mode 100644 index 42348422..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CliCommandConverter.kt +++ /dev/null @@ -1,140 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.bindText -import com.intellij.util.system.OS -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import java.lang.System.lineSeparator - -internal class CliCommandConverter( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : TextConverter( - textConverterContext = TextConverterContext( - convertActionTitle = "Add line breaks", - revertActionTitle = "Remove line breaks", - sourceTitle = "Without line breaks", - targetTitle = "With line breaks", - diffSupport = DiffSupport( - title = "CLI Command Formatting" - ), - defaultSourceText = DEFAULT_SOURCE_TEXT, - defaultTargetText = defaultTargetText - ), - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val lineBreakDelimiter = configuration.register("lineBreakDelimiter", defaultLineBreakDelimiter) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun Panel.buildMiddleFirstConfigurationUi() { - row { - textField() - .label("Line break delimiter:") - .bindText(lineBreakDelimiter) - } - } - - override fun toTarget(text: String) { - targetText.set( - doToTarget( - text, - lineBreakDelimiter.get() - ) - ) - } - - override fun toSource(text: String) { - val result = cliCommandAllArgumentsSplitRegex - .findAll(text) - .mapNotNull { matchResult -> - matchResult.groups[1]?.let { "\"${it.value}\"" } ?: matchResult.groups[2]?.let { "'${it.value}'" } ?: matchResult.groups[0]?.value - } - .map { it.trim() } - .filter { it != lineBreakDelimiter.get() } - .joinToString(" ") - sourceText.set(result) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "CLI Command", - contentTitle = "CLI Command Line Breaks" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> CliCommandConverter) = { configuration -> - CliCommandConverter(configuration, parentDisposable, context, project) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val CLI_COMMAND_INDENT = " " - private val defaultLineBreakDelimiter = if (OS.CURRENT == OS.Windows) "^" else "\\" - private val cliCommandAllArgumentsSplitRegex = Regex("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'") - - private const val DEFAULT_SOURCE_TEXT = "python script.py --input-file=input.txt --output-file=output.txt --verbose" - private val defaultTargetText = - doToTarget( - DEFAULT_SOURCE_TEXT, - defaultLineBreakDelimiter - ) - - private fun doToTarget(text: String, lineBreakDelimiter: String): String { - val lines = mutableListOf() - - var currentLine = "" - cliCommandAllArgumentsSplitRegex - .findAll(text) - .mapNotNull { matchResult -> - matchResult.groups[1]?.let { "\"${it.value}\"" } ?: matchResult.groups[2]?.let { "'${it.value}'" } ?: matchResult.groups[0]?.value - } - .map { it.trim() } - .forEach { token -> - currentLine = if (token.startsWith("-")) { - lines.add(currentLine) - - if (lines.size >= 1) { - "$CLI_COMMAND_INDENT$token" - } - else { - token - } - } - else if (currentLine.isBlank()) { - token - } - else { - "$currentLine $token" - } - } - lines.add(currentLine) - - return lines.filter { it.isNotBlank() } - .joinToString(" ${lineBreakDelimiter}${lineSeparator()}") - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CodeFormattingConverter.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CodeFormattingConverter.kt deleted file mode 100644 index 67d33576..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CodeFormattingConverter.kt +++ /dev/null @@ -1,157 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper -import com.fasterxml.jackson.dataformat.toml.TomlMapper -import com.fasterxml.jackson.dataformat.xml.XmlMapper -import com.fasterxml.jackson.dataformat.yaml.YAMLMapper -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.bindItem -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -internal class CodeFormattingConverter( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : TextConverter( - textConverterContext = TextConverterContext( - convertActionTitle = "Convert", - revertActionTitle = "Convert", - sourceTitle = "First", - targetTitle = "Second", - sourceErrorHolder = ErrorHolder(), - targetErrorHolder = ErrorHolder(), - diffSupport = DiffSupport( - title = "Text Format Converter" - ) - ), - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project -), DeveloperToolConfiguration.ChangeListener { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var firstLanguage = configuration.register("firstLanguage", Language.JSON) - private var secondLanguage = configuration.register("secondLanguage", Language.YAML) - - private val codeStyles by lazy { LanguageCodeStyleSettingsProvider.EP_NAME.extensionList.associate { it.language.id to it.language } } - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun configurationChanged(property: ValueProperty) { - setLanguages() - super.configurationChanged(property) - } - - override fun Panel.buildTopConfigurationUi() { - row { - comboBox(Language.entries) - .label("First language:") - .bindItem(firstLanguage) - } - } - - override fun Panel.buildMiddleSecondConfigurationUi() { - row { - comboBox(Language.entries) - .label("Second language:") - .bindItem(secondLanguage) - } - } - - override fun toTarget(text: String) { - covert(textConverterContext.sourceErrorHolder!!) { - if (text.isBlank()) { - targetText.set("") - } - else { - targetText.set(secondLanguage.get().asString(firstLanguage.get().parse(text))) - } - } - } - - override fun toSource(text: String) { - covert(textConverterContext.targetErrorHolder!!) { - if (text.isBlank()) { - sourceText.set("") - } - else { - sourceText.set(firstLanguage.get().asString(secondLanguage.get().parse(text))) - } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun covert(inputErrorHolder: ErrorHolder, doConvert: () -> Unit) { - // We have to clear both `ErrorHolder`s here. If he user makes an invalid - // input in A, which shows an error, and then edits B, the contents of A - // would be replaced, but the error message is still visible. - textConverterContext.targetErrorHolder!!.clear() - textConverterContext.sourceErrorHolder!!.clear() - - try { - doConvert() - } catch (e: Exception) { - inputErrorHolder.add(e) - } - - // The `validate` in this class is not used as a validation mechanism. We - // make use of its text field error UI to display the `errorHolder`. - validate() - } - - private fun setLanguages() { - codeStyles[firstLanguage.get().languageId]?.let { setSourceLanguage(it) } - codeStyles[secondLanguage.get().languageId]?.let { setTargetLanguage(it) } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class Language(val title: String, val languageId: String, val objectMapper: ObjectMapper) { - - JSON("JSON", "JSON", ObjectMapper()), - YAML("YAML", "YAML", YAMLMapper()), - XML("XML", "XML", XmlMapper()), - TOML("TOML", "TOML", TomlMapper()), - PROPERTIES("Properties", "Properties", JavaPropsMapper()); - - override fun toString(): String = title - - fun parse(text: String): JsonNode = objectMapper.readTree(text) - - fun asString(root: JsonNode): String = objectMapper.writeValueAsString(root) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Text Format", - contentTitle = "Text Format Converter" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> CodeFormattingConverter) = { configuration -> - CodeFormattingConverter(configuration, parentDisposable, context, project) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/DatetimeConverter.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/DatetimeConverter.kt deleted file mode 100644 index ccb5fa72..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/DatetimeConverter.kt +++ /dev/null @@ -1,619 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter - -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.DataKey -import com.intellij.openapi.observable.properties.ObservableProperty -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.ui.getUserData -import com.intellij.openapi.ui.putUserData -import com.intellij.openapi.util.Key -import com.intellij.openapi.util.text.StringUtil.stripHtml -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.COLUMNS_MEDIUM -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.Row -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.actionButton -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.columns -import com.intellij.ui.dsl.builder.text -import com.intellij.ui.dsl.builder.whenItemSelectedFromUi -import com.intellij.ui.dsl.builder.whenStateChangedFromUi -import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import com.intellij.ui.layout.ComboBoxPredicate -import com.intellij.util.Alarm -import com.intellij.util.text.OrdinalFormat.formatEnglish -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.CopyAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer.Companion.ALL_AVAILABLE_LOCALES -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bindIntTextImproved -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bindLongTextImproved -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.changeFont -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.not -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateLongValue -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.DAY -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.HOUR -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.MINUTE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.MONTH -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.SECOND -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.TIME_ZONE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.UNIX_TIMESTAMP_MILLIS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.UNIX_TIMESTAMP_SECONDS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter.ConversionOrigin.YEAR -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import java.awt.Font -import java.time.Duration -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.time.format.TextStyle.FULL_STANDALONE -import java.time.temporal.ChronoField -import java.time.temporal.IsoFields -import java.util.* - -class DatetimeConverter( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - private val context: DeveloperUiToolContext -) : DeveloperUiTool(parentDisposable) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var selectedTimeZoneId = configuration.register("timeZoneId", ZoneId.systemDefault().id) - private var formattedStandardFormat = configuration.register("formattedStandardFormat", DEFAULT_FORMATTED_STANDARD_FORMAT) - private var formattedStandardFormatAddOffset = configuration.register("formattedStandardFormatAddOffset", DEFAULT_FORMATTED_STANDARD_FORMAT_ADD_OFFSET) - private var formattedStandardFormatAddTimeZone = configuration.register("formattedStandardFormatAddTimeZone", DEFAULT_FORMATTED_STANDARD_FORMAT_ADD_TIME_ZONE) - private var formattedIndividual = configuration.register("formattedIndividual", DEFAULT_FORMATTED_INDIVIDUAL) - private var formattedLocale = configuration.register("formattedLocale", DEFAULT_FORMATTED_LOCALE) - private var formattedIndividualFormat = configuration.register("formattedIndividualFormat", DEFAULT_INDIVIDUAL_FORMAT) - - private var formattedStandardFormatPattern = ValueProperty("") - private var formattedText = ValueProperty("No result") - private var dateDetails = ValueProperty("") - - private val currentUnixTimestampUpdateAlarm by lazy { Alarm(parentDisposable) } - private val currentUnixTimestampUpdate: Runnable by lazy { createCurrentUnixTimestampUpdate() } - private val currentUnixTimestampSeconds = ValueProperty(System.currentTimeMillis().div(1000).toString()) - private val currentUnixTimestampMillis = ValueProperty(System.currentTimeMillis().toString()) - - private val convertAlarm by lazy { Alarm(parentDisposable) } - - private val convertUnixTimeStampSeconds = ValueProperty(0) - private val convertUnixTimeStampMillis = ValueProperty(0) - private val convertDay = ValueProperty(0) - private val convertMonth = ValueProperty(0) - private val convertYear = ValueProperty(0) - private val convertHour = ValueProperty(0) - private val convertMinute = ValueProperty(0) - private val convertSecond = ValueProperty(0) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - // Validate if selected time zone and formatted local is still available - if (!ZoneId.getAvailableZoneIds().contains(selectedTimeZoneId.get())) { - selectedTimeZoneId.set(ZoneId.systemDefault().id) - } - if (!ALL_AVAILABLE_LOCALES.contains(formattedLocale.get())) { - formattedLocale.set(DEFAULT_FORMATTED_LOCALE) - } - } - - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - group("Current Unix Timestamp") { - if (context.prioritizeVerticalLayout) { - row { - buildTimestampLabelUi("Seconds:", currentUnixTimestampSeconds, TIMESTAMP_SECONDS_CONTENT_DATA_KEY) - } - row { - buildTimestampLabelUi("Milliseconds:", currentUnixTimestampMillis, TIMESTAMP_MILLIS_CONTENT_DATA_KEY) - } - } - else { - row { - buildTimestampLabelUi("Seconds:", currentUnixTimestampSeconds, TIMESTAMP_SECONDS_CONTENT_DATA_KEY) - buildTimestampLabelUi("Milliseconds:", currentUnixTimestampMillis, TIMESTAMP_MILLIS_CONTENT_DATA_KEY) - } - } - } - - group("Convert") { - val initialInstant = Instant.ofEpochMilli(System.currentTimeMillis()) - val initialLocalDateTime = LocalDateTime.ofInstant(initialInstant, selectedTimeZoneId()) - - group("Unix Timestamp") { - if (context.prioritizeVerticalLayout) { - row { - buildUnixTimeStampSecondsTextFieldUi(initialInstant) - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID) - row { - buildUnixTimeStampMillisTextFieldUi(initialInstant) - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE).layout(RowLayout.PARENT_GRID) - row { - buildSetToNowButtonUi() - } - } - else { - row { - buildUnixTimeStampSecondsTextFieldUi(initialInstant) - buildUnixTimeStampMillisTextFieldUi(initialInstant) - buildSetToNowButtonUi() - } - } - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - group("Date and Time") { - row { - comboBox(ZoneId.getAvailableZoneIds().sorted()) - .label("Time zone:") - .bindItem(selectedTimeZoneId) - .whenItemSelectedFromUi { convert(TIME_ZONE) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, TIME_ZONE) } - } - data class DateField( - val title: String, - val initialValue: Int, - val valueProperty: ValueProperty, - val range: LongRange, - val changeOrigin: ConversionOrigin - ) - row { - listOf( - DateField("Year", initialLocalDateTime.year, convertYear, LongRange(1970, 9999), YEAR), - DateField("Month", initialLocalDateTime.monthValue, convertMonth, LongRange(1, 12), MONTH), - DateField("Day", initialLocalDateTime.dayOfMonth, convertDay, LongRange(1, 31), DAY) - ).forEach { (title, initialValue, valueProperty, range, changeOrigin) -> - textField().label("$title:") - .text(initialValue.toString()) - .bindIntTextImproved(valueProperty) - .columns(5) - .validateLongValue(range) - .whenTextChangedFromUi { convert(changeOrigin) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, changeOrigin) } - } - }.layout(RowLayout.PARENT_GRID) - row { - listOf( - DateField("Hour", initialLocalDateTime.hour, convertHour, LongRange(0, 23), HOUR), - DateField("Minute", initialLocalDateTime.minute, convertMinute, LongRange(0, 59), MINUTE), - DateField("Second", initialLocalDateTime.second, convertSecond, LongRange(0, 59), SECOND) - ).forEach { (title, initialValue, valueProperty, range, changeOrigin) -> - textField().label("$title:") - .text(initialValue.toString()) - .bindIntTextImproved(valueProperty) - .columns(5) - .validateLongValue(range) - .whenTextChangedFromUi { convert(changeOrigin) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, changeOrigin) } - } - }.layout(RowLayout.PARENT_GRID) - row { - comment("") - .bindText(dateDetails) - } - }.layout(RowLayout.PARENT_GRID).topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - group("Formatted") { - buttonsGroup { - lateinit var formattedStandardFormatComboBox: ComboBox - row { - radioButton("Standard format:") - .bindSelected(formattedIndividual.not()) - .onChanged { convert(UNIX_TIMESTAMP_MILLIS) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } - .gap(RightGap.SMALL) - formattedStandardFormatComboBox = comboBox(StandardFormat.entries) - .bindItem(formattedStandardFormat) - .columns(COLUMNS_MEDIUM) - .whenItemSelectedFromUi { syncFormattedStandardFormatPattern(); convert(UNIX_TIMESTAMP_MILLIS) } - .enabledIf(formattedIndividual.not()) - .gap(RightGap.SMALL) - .component - .apply { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } - if (!context.prioritizeVerticalLayout) { - buildStandardFormatConfigurationUi(formattedStandardFormatComboBox) - } - }.layout(RowLayout.PARENT_GRID).bottomGap(BottomGap.NONE) - if (context.prioritizeVerticalLayout) { - row { - cell() - buildStandardFormatConfigurationUi(formattedStandardFormatComboBox) - }.topGap(TopGap.NONE).layout(RowLayout.PARENT_GRID) - } - row { - cell() - comment("") - .bindText(formattedStandardFormatPattern) - .enabledIf(formattedIndividual.not()) - }.topGap(TopGap.NONE).layout(RowLayout.PARENT_GRID) - - row { - radioButton("Individual format:") - .bindSelected(formattedIndividual) - .onChanged { convert(UNIX_TIMESTAMP_MILLIS) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } - .gap(RightGap.SMALL) - expandableTextField() - .bindText(formattedIndividualFormat) - .columns(COLUMNS_MEDIUM) - .whenTextChangedFromUi { convert(UNIX_TIMESTAMP_MILLIS) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } - .validationInfo { - try { - if (formattedIndividual.get()) { - DateTimeFormatter.ofPattern(it.text) - } - return@validationInfo null - } catch (_: Exception) { - return@validationInfo ValidationInfo("Invalid individual format", it) - } - } - .enabledIf(formattedIndividual) - }.layout(RowLayout.PARENT_GRID) - } - - row { - comboBox(ALL_AVAILABLE_LOCALES) - .label("Locale:") - .bindItem(formattedLocale) - .columns(COLUMNS_MEDIUM) - .onChanged { convert(UNIX_TIMESTAMP_MILLIS) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } - }.layout(RowLayout.PARENT_GRID) - - row { - label("") - .bindText(formattedText) - .changeFont(scale = 1.1f, style = Font.BOLD) - .gap(RightGap.SMALL) - actionButton(CopyAction(FORMATTED_TEXT_DATA_KEY), DatetimeConverter::class.java.name) - }.topGap(TopGap.SMALL) - }.topGap(TopGap.NONE) - } - } - - override fun afterBuildUi() { - init() - } - - override fun reset() { - init() - } - - override fun getData(dataId: String): Any? = when { - TIMESTAMP_SECONDS_CONTENT_DATA_KEY.`is`(dataId) -> stripHtml(currentUnixTimestampSeconds.get(), false) - TIMESTAMP_MILLIS_CONTENT_DATA_KEY.`is`(dataId) -> stripHtml(currentUnixTimestampMillis.get(), false) - FORMATTED_TEXT_DATA_KEY.`is`(dataId) -> stripHtml(formattedText.get(), false) - else -> null - } - - override fun activated() { - scheduleCurrentUnixTimestampUpdate(0) - } - - override fun deactivated() { - currentUnixTimestampUpdateAlarm.cancelAllRequests() - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun Row.buildSetToNowButtonUi() { - button("Set to Now") { - convertUnixTimeStampMillis.set(System.currentTimeMillis()) - convert(UNIX_TIMESTAMP_MILLIS, 0) - } - } - - private fun Row.buildUnixTimeStampMillisTextFieldUi(initialInstant: Instant) { - textField().validateLongValue(LongRange(0, Long.MAX_VALUE)) - .bindLongTextImproved(convertUnixTimeStampMillis) - .label("Milliseconds:") - .text(initialInstant.toEpochMilli().toString()) - .columns(12) - .whenTextChangedFromUi { convert(UNIX_TIMESTAMP_MILLIS) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_MILLIS) } - } - - private fun Row.buildUnixTimeStampSecondsTextFieldUi(initialInstant: Instant) { - textField().validateLongValue(LongRange(0, Long.MAX_VALUE)) - .bindLongTextImproved(convertUnixTimeStampSeconds) - .label("Seconds:") - .text(initialInstant.epochSecond.toString()) - .columns(12) - .whenTextChangedFromUi { convert(UNIX_TIMESTAMP_SECONDS) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_SECONDS) } - } - - private fun Row.buildStandardFormatConfigurationUi(formattedStandardFormatComboBox: ComboBox) { - checkBox("Add offset") - .bindSelected(formattedStandardFormatAddOffset) - .whenStateChangedFromUi { syncFormattedStandardFormatPattern(); convert(UNIX_TIMESTAMP_MILLIS) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_SECONDS) } - .enabledIf(formattedIndividual.not()) - .visibleIf(ComboBoxPredicate(formattedStandardFormatComboBox) { it?.supportsOffset ?: false }) - .gap(RightGap.SMALL) - checkBox("Add time zone") - .bindSelected(formattedStandardFormatAddTimeZone) - .whenStateChangedFromUi { syncFormattedStandardFormatPattern(); convert(UNIX_TIMESTAMP_MILLIS) } - .applyToComponent { putUserData(CONVERSION_ORIGIN_KEY, UNIX_TIMESTAMP_SECONDS) } - .enabledIf(formattedIndividual.not()) - .visibleIf(ComboBoxPredicate(formattedStandardFormatComboBox) { it?.supportsTimeZone ?: false }) - } - - private fun init() { - syncFormattedStandardFormatPattern() - - convertUnixTimeStampMillis.set(System.currentTimeMillis()) - convert(UNIX_TIMESTAMP_MILLIS, 0) - } - - private fun syncFormattedStandardFormatPattern() { - val pattern = formattedStandardFormat.get().buildPattern( - offset = formattedStandardFormatAddOffset.get(), - timeZone = formattedStandardFormatAddTimeZone.get() - ) - formattedStandardFormatPattern.set(pattern) - } - - private fun createCurrentUnixTimestampUpdate(): Runnable = Runnable { - currentUnixTimestampSeconds.set("${System.currentTimeMillis().div(1000)}") - currentUnixTimestampMillis.set("${System.currentTimeMillis()}") - scheduleCurrentUnixTimestampUpdate() - } - - private fun scheduleCurrentUnixTimestampUpdate(delayMillis: Long = TIMESTAMP_UPDATE_INTERVAL_MILLIS) { - currentUnixTimestampUpdateAlarm.addRequest(currentUnixTimestampUpdate, delayMillis) - } - - private fun convert(conversionOrigin: ConversionOrigin, delayMillis: Long = 100) { - if (validate().any { it.component?.getUserData(CONVERSION_ORIGIN_KEY)?.kind == conversionOrigin.kind }) { - return - } - - // Take a snapshot of the input fields since the alarm gets execute with - // some delay. - val unixTimeStampSeconds = convertUnixTimeStampSeconds.get() - val unixTimeStampMillis = convertUnixTimeStampMillis.get() - - val convert: () -> Unit = { - when (conversionOrigin) { - UNIX_TIMESTAMP_SECONDS -> { - val timestampAtSelectedTimeZone = Instant.ofEpochSecond(unixTimeStampSeconds) - .atZone(selectedTimeZoneId()) - setConvertedValues(timestampAtSelectedTimeZone, conversionOrigin) - } - - TIME_ZONE, UNIX_TIMESTAMP_MILLIS -> { - val timestampAtSelectedTimeZone = Instant.ofEpochMilli(unixTimeStampMillis) - .atZone(selectedTimeZoneId()) - setConvertedValues(timestampAtSelectedTimeZone, conversionOrigin) - } - - DAY, MONTH, YEAR, HOUR, MINUTE, SECOND -> { - val year = convertYear.get() - val month = convertMonth.get() - val day = convertDay.get() - val hour = convertHour.get() - val minute = convertMinute.get() - val second = convertSecond.get() - - val localDateTime = ZonedDateTime.of(year, month, day, hour, minute, second, 0, selectedTimeZoneId()) - setConvertedValues(localDateTime, conversionOrigin) - } - } - - // Trigger validation again to show errors from `ErrorHolder`s - validate() - } - if (!isDisposed && !convertAlarm.isDisposed) { - convertAlarm.cancelAllRequests() - convertAlarm.addRequest(convert, delayMillis) - } - } - - private fun setConvertedValues(localDateTime: ZonedDateTime, conversionOrigin: ConversionOrigin) { - val millis = localDateTime.withZoneSameInstant(ZoneOffset.UTC).toInstant().toEpochMilli() - if (conversionOrigin != UNIX_TIMESTAMP_SECONDS) { - convertUnixTimeStampSeconds.set(millis.div(1000)) - } - if (conversionOrigin != UNIX_TIMESTAMP_MILLIS) { - convertUnixTimeStampMillis.set(millis) - } - if (conversionOrigin != YEAR) { - convertYear.set(localDateTime.year) - } - if (conversionOrigin != MONTH) { - convertMonth.set(localDateTime.monthValue) - } - if (conversionOrigin != DAY) { - convertDay.set(localDateTime.dayOfMonth) - } - if (conversionOrigin != HOUR) { - convertHour.set(localDateTime.hour) - } - if (conversionOrigin != MINUTE) { - convertMinute.set(localDateTime.minute) - } - if (conversionOrigin != SECOND) { - convertSecond.set(localDateTime.second) - } - - val dayOfYear = localDateTime.get(ChronoField.DAY_OF_YEAR).toLong() - val weekNumber = localDateTime.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR).toLong() - val quarterOfYear = localDateTime.get(IsoFields.QUARTER_OF_YEAR).toLong() - val dayName = localDateTime.dayOfWeek.getDisplayName(FULL_STANDALONE, Locale.getDefault()) - if (context.prioritizeVerticalLayout) { - dateDetails.set( - "$dayName; ${formatEnglish(dayOfYear)} day of the year; ${formatEnglish(weekNumber)} week; ${ - formatEnglish( - quarterOfYear - ) - } quarter" - ) - } - else { - dateDetails.set( - "A $dayName, the ${formatEnglish(dayOfYear)} day of the year, in the ${formatEnglish(weekNumber)} week, within the ${ - formatEnglish( - quarterOfYear - ) - } quarter." - ) - } - - formattedText.set("${formatDateTime(localDateTime).ifBlank { "No result" }}") - } - - private fun formatDateTime(localDateTime: ZonedDateTime): String = - try { - val formatter = if (formattedIndividual.get()) { - DateTimeFormatter.ofPattern(formattedIndividualFormat.get()) - .withLocale(formattedLocale.get().locale) - ?.withZone(selectedTimeZoneId()) - } - else { - DateTimeFormatter.ofPattern(formattedStandardFormatPattern.get()) - .withLocale(formattedLocale.get().locale) - .withZone(formattedStandardFormat.get().fixedTimeZone ?: selectedTimeZoneId()) - } - localDateTime.format(formatter) - } catch (e: Exception) { - "Error: ${e.message}" - } - - private fun selectedTimeZoneId(): ZoneId = ZoneId.of(selectedTimeZoneId.get()) - - private fun Row.buildTimestampLabelUi( - title: String, - timestampProperty: ObservableProperty, - contentDataKey: DataKey - ) { - panel { - row { - label(title).gap(RightGap.SMALL) - label("") - .changeFont(scale = 1.5f, style = Font.BOLD) - .bindText(timestampProperty) - .gap(RightGap.SMALL) - actionButton(CopyAction(contentDataKey)) - }.topGap(TopGap.NONE) - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Date and Time", - contentTitle = "Date and Time Converter" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> DatetimeConverter) = - { configuration -> DatetimeConverter(configuration, parentDisposable, context) } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class ConversionOriginKind { - - TIME_ZONE, - UNIX_TIMESTAMP_SECONDS, - UNIX_TIMESTAMP_MILLIS, - LOCAL_DATE_TIME - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class ConversionOrigin(val kind: ConversionOriginKind) { - - TIME_ZONE(ConversionOriginKind.TIME_ZONE), - UNIX_TIMESTAMP_SECONDS(ConversionOriginKind.UNIX_TIMESTAMP_SECONDS), - UNIX_TIMESTAMP_MILLIS(ConversionOriginKind.UNIX_TIMESTAMP_MILLIS), - DAY(ConversionOriginKind.LOCAL_DATE_TIME), - MONTH(ConversionOriginKind.LOCAL_DATE_TIME), - YEAR(ConversionOriginKind.LOCAL_DATE_TIME), - HOUR(ConversionOriginKind.LOCAL_DATE_TIME), - MINUTE(ConversionOriginKind.LOCAL_DATE_TIME), - SECOND(ConversionOriginKind.LOCAL_DATE_TIME) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class StandardFormat( - private val title: String, - private val pattern: String, - val supportsOffset: Boolean = true, - val supportsTimeZone: Boolean = true, - val fixedTimeZone: ZoneId? = null - ) { - - ISO_8601("ISO-8601 date time", "yyyy-MM-dd'T'HH:mm:ss.SSS"), - ISO_8601_UTC( - title = "ISO-8601 date time at UTC", - pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", - supportsOffset = false, - supportsTimeZone = false, - fixedTimeZone = ZoneOffset.UTC - ), - ISO_8601_DATE("ISO-8601 date", "yyyy-MM-dd"), - ISO_8601_TIME_WITH("ISO-8601 time", "HH:mm:ss"), - ISO_8601_ORDINAL_DATE("ISO-8601 ordinal date", "yyyy-DDD"), - ISO_8601_WEEK_DATE("ISO-8601 week date", "YYYY-'W'ww-e"), - RFC_1123_DATE_TIME("RFC-1123 date time", "EEE, dd MMM yyyy HH:mm:ss"); - - fun buildPattern(offset: Boolean, timeZone: Boolean): String { - val patternBuilder = StringBuilder(pattern) - if (offset && supportsOffset) { - patternBuilder.append("XXX") - } - if (timeZone && supportsTimeZone) { - patternBuilder.append("'['VV']'") - } - return patternBuilder.toString() - } - - override fun toString(): String = title - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val TIMESTAMP_SECONDS_CONTENT_DATA_KEY = DataKey.create("timestampSeconds") - private val TIMESTAMP_MILLIS_CONTENT_DATA_KEY = DataKey.create("timestampMillis") - private val FORMATTED_TEXT_DATA_KEY = DataKey.create("formattedText") - - private val TIMESTAMP_UPDATE_INTERVAL_MILLIS: Long = Duration.ofSeconds(1).toMillis() - - private const val DEFAULT_FORMATTED_INDIVIDUAL = false - private val DEFAULT_FORMATTED_LOCALE = LocaleContainer(Locale.getDefault()) - private const val DEFAULT_INDIVIDUAL_FORMAT = "" - private val DEFAULT_FORMATTED_STANDARD_FORMAT = StandardFormat.ISO_8601_UTC - private const val DEFAULT_FORMATTED_STANDARD_FORMAT_ADD_OFFSET = true - private const val DEFAULT_FORMATTED_STANDARD_FORMAT_ADD_TIME_ZONE = false - - private val CONVERSION_ORIGIN_KEY = Key("conversionOrigin") - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/EncodersDecoders.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/EncodersDecoders.kt deleted file mode 100644 index 14181831..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/EncodersDecoders.kt +++ /dev/null @@ -1,273 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import dev.turingcomplete.intellijdevelopertoolsplugin.common.decodeFromAscii -import dev.turingcomplete.intellijdevelopertoolsplugin.common.encodeToAscii -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import org.apache.commons.codec.binary.Base32 -import java.net.URLDecoder -import java.net.URLEncoder -import java.nio.charset.StandardCharsets -import java.util.* - -// -- Properties ---------------------------------------------------------------------------------------------------- // - -internal val encoderDecoderTextConverterContext = TextConverter.TextConverterContext( - convertActionTitle = "Encode", - revertActionTitle = "Decode", - sourceTitle = "Decoded", - targetTitle = "Encoded" -) - -// -- Exposed Methods ----------------------------------------------------------------------------------------------- // -// -- Private Methods ----------------------------------------------------------------------------------------------- // -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class Base32EncoderDecoder( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = encoderDecoderTextConverterContext, - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(Base32().encodeToString(text.encodeToByteArray())) - } - - override fun toSource(text: String) { - sourceText.set(Base32().decode(text).decodeToString()) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Base32 Encoding", - groupedMenuTitle = "Base32", - contentTitle = "Base32 Encoder/Decoder" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> Base32EncoderDecoder) = - { configuration -> Base32EncoderDecoder(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class Base64EncoderDecoder( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = encoderDecoderTextConverterContext, - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(Base64.getEncoder().encodeToString(text.encodeToByteArray())) - } - - override fun toSource(text: String) { - sourceText.set(Base64.getDecoder().decode(text).decodeToString()) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Base64 Encoding", - groupedMenuTitle = "Base64", - contentTitle = "Base64 Encoder/Decoder" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> Base64EncoderDecoder) = - { configuration -> Base64EncoderDecoder(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class UrlBase64EncoderDecoder( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = encoderDecoderTextConverterContext, - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(Base64.getUrlEncoder().encodeToString(text.encodeToByteArray())) - } - - override fun toSource(text: String) { - sourceText.set(Base64.getUrlDecoder().decode(text).decodeToString()) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "URL Base64 Encoding", - groupedMenuTitle = "URL Base64", - contentTitle = "URL Base64 Encoder/Decoder" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> UrlBase64EncoderDecoder) = - { configuration -> UrlBase64EncoderDecoder(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class MimeBase64EncoderDecoder( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = encoderDecoderTextConverterContext, - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(Base64.getMimeEncoder().encodeToString(text.encodeToByteArray())) - } - - override fun toSource(text: String) { - sourceText.set(Base64.getMimeDecoder().decode(text).decodeToString()) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "MIME Base64 Encoding", - groupedMenuTitle = "MIME Base64", - contentTitle = "MIME Base64 Encoder/Decoder" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> MimeBase64EncoderDecoder) = - { configuration -> MimeBase64EncoderDecoder(configuration, parentDisposable, context, project) } - } -} -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class AsciiEncoderDecoder( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = encoderDecoderTextConverterContext, - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(text.encodeToAscii()) - } - - override fun toSource(text: String) { - sourceText.set(text.decodeFromAscii()) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "ASCII Encoding", - groupedMenuTitle = "ASCII", - contentTitle = "ASCII Encoder/Decoder" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> AsciiEncoderDecoder) = - { configuration -> AsciiEncoderDecoder(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class UrlEncodingEncoderDecoder( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = encoderDecoderTextConverterContext, - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(URLEncoder.encode(text, StandardCharsets.UTF_8)) - } - - override fun toSource(text: String) { - sourceText.set(URLDecoder.decode(text, StandardCharsets.UTF_8)) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "URL Encoding", - groupedMenuTitle = "URL", - contentTitle = "URL Encoding Encoder/Decoder" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> UrlEncodingEncoderDecoder) = - { configuration -> UrlEncodingEncoderDecoder(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/LineBreaksEncoderDecoder.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/LineBreaksEncoderDecoder.kt deleted file mode 100644 index f25e436a..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/LineBreaksEncoderDecoder.kt +++ /dev/null @@ -1,87 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.text.StringUtil -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.bindItem -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -internal class LineBreaksEncoderDecoder( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : TextConverter( - textConverterContext = encoderDecoderTextConverterContext, - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var lineBreakDecoding = configuration.register("lineBreakDecoding", LineBreak.CRLF) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun toTarget(text: String) { - targetText.set( - when (lineBreakDecoding.get()) { - LineBreak.CRLF -> StringUtil.convertLineSeparators(text, "\\r\\n") - LineBreak.LF -> StringUtil.convertLineSeparators(text, "\\n") - } - ) - } - - override fun toSource(text: String) { - // The target input is not depending on the selected line break decoding, - // because the user can put anything into the editor without changing the - // configuration first. - sourceText.set( - text.replace("\\r\\n", System.lineSeparator()).replace("\\n", System.lineSeparator()) - ) - } - - override fun Panel.buildMiddleFirstConfigurationUi() { - row { - comboBox(LineBreak.entries) - .label("Decode line break to:") - .bindItem(lineBreakDecoding) - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class LineBreak(val title: String) { - - CRLF("\\r\\n"), - LF("\\n"); - - override fun toString(): String = title - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Line Breaks", - contentTitle = "Line Breaks Encoder/Decoder" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> LineBreaksEncoderDecoder) = - { configuration -> LineBreaksEncoderDecoder(configuration, parentDisposable, context, project) } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/TextConverter.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/TextConverter.kt deleted file mode 100644 index e7705f67..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/TextConverter.kt +++ /dev/null @@ -1,283 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter - -import com.intellij.icons.AllIcons -import com.intellij.lang.Language -import com.intellij.openapi.Disposable -import com.intellij.openapi.observable.properties.AtomicProperty -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.selected -import com.intellij.ui.layout.not -import com.intellij.util.Alarm -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor.EditorMode.INPUT_OUTPUT -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.PropertyComponentPredicate -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.TextConverter.ActiveInput.SOURCE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.TextConverter.ActiveInput.TARGET -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext - -internal abstract class TextConverter( - protected val textConverterContext: TextConverterContext, - protected val configuration: DeveloperToolConfiguration, - protected val context: DeveloperUiToolContext, - protected val project: Project?, - parentDisposable: Disposable -) : DeveloperUiTool(parentDisposable), DeveloperToolConfiguration.ChangeListener { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var liveConversion = configuration.register("liveConversion", true) - protected var sourceText = configuration.register("sourceText", textConverterContext.defaultSourceText, INPUT) - protected var targetText = configuration.register("targetText", textConverterContext.defaultTargetText, INPUT) - - private val conversionAlarm by lazy { Alarm(parentDisposable) } - - private var lastActiveInput = AtomicProperty(SOURCE) - private val toSourceActive = PropertyComponentPredicate(lastActiveInput, TARGET) - - private val sourceEditor by lazy { createSourceEditor() } - private val targetEditor by lazy { createTargetEditor() } - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - liveConversion.afterChange(parentDisposable) { - liveTransformToLastActiveInput() - } - } - - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - buildTopConfigurationUi() - - row { - resizableRow() - val sourceEditorCell = cell(sourceEditor.component).align(Align.FILL) - textConverterContext.sourceErrorHolder?.let { sourceErrorHolder -> - sourceEditorCell - .validationOnApply(sourceEditor.bindValidator(sourceErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - } - } - - buildMiddleFirstConfigurationUi() - buildActionsUi() - buildMiddleSecondConfigurationUi() - - row { - resizableRow() - val targetEditorCell = cell(targetEditor.component).align(Align.FILL) - textConverterContext.targetErrorHolder?.let { targetErrorHolder -> - targetEditorCell - .validationOnApply(targetEditor.bindValidator(targetErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - } - } - } - - protected open fun Panel.buildTopConfigurationUi() { - // Override if needed - } - - protected open fun Panel.buildMiddleFirstConfigurationUi() { - // Override if needed - } - - protected open fun Panel.buildMiddleSecondConfigurationUi() { - // Override if needed - } - - protected fun setSourceLanguage(language: Language) { - sourceEditor.language = language - } - - protected fun setTargetLanguage(language: Language) { - targetEditor.language = language - } - - abstract fun toTarget(text: String) - - abstract fun toSource(text: String) - - fun targetText(): String = targetText.get() - - fun sourceText(): String = sourceText.get() - - override fun configurationChanged(property: ValueProperty) { - liveTransformToLastActiveInput() - } - - override fun activated() { - configuration.addChangeListener(parentDisposable, this) - } - - override fun deactivated() { - configuration.removeChangeListener(this) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun Panel.buildActionsUi() { - buttonsGroup { - row { - val liveConversionCheckBox = checkBox("Live conversion") - .bindSelected(liveConversion) - .gap(RightGap.SMALL) - icon(AllIcons.General.ArrowUp) - .visibleIf(toSourceActive) - .enabledIf(liveConversion) - .gap(RightGap.SMALL) - icon(AllIcons.General.ArrowDown) - .visibleIf(toSourceActive.not()) - .enabledIf(liveConversion) - .gap(RightGap.SMALL) - - button("▼ ${textConverterContext.convertActionTitle}") { transformToTarget() } - .enabledIf(liveConversionCheckBox.selected.not()) - .gap(RightGap.SMALL) - button("▲ ${textConverterContext.revertActionTitle}") { transformToSource() } - .enabledIf(liveConversionCheckBox.selected.not()) - } - } - } - - private fun transformToSource() { - doToSource(targetEditor.text) - } - - private fun transformToTarget() { - doToTarget(sourceEditor.text) - } - - private fun doToTarget(text: String) { - doConversion { - try { - toTarget(text) - } catch (_: Exception) { - } - } - } - - private fun doToSource(text: String) { - doConversion { - try { - toSource(text) - } catch (_: Exception) { - } - } - } - - private fun doConversion(conversion: () -> Unit) { - if (!isDisposed && !conversionAlarm.isDisposed) { - conversionAlarm.cancelAllRequests() - conversionAlarm.addRequest(conversion, 0) - } - } - - private fun createSourceEditor() = - DeveloperToolEditor( - id = "source", - title = textConverterContext.sourceTitle, - editorMode = INPUT_OUTPUT, - parentDisposable = parentDisposable, - configuration = configuration, - context = context, - project = project, - textProperty = sourceText, - diffSupport = textConverterContext.diffSupport?.let { diffSupport -> - DeveloperToolEditor.DiffSupport( - title = diffSupport.title, - secondTitle = textConverterContext.targetTitle, - secondText = { targetText.get() }, - ) - } - ).apply { - onFocusGained { - lastActiveInput.set(SOURCE) - } - this.onTextChangeFromUi { text -> - if (liveConversion.get()) { - lastActiveInput.set(SOURCE) - doToTarget(text) - } - } - } - - private fun createTargetEditor(): DeveloperToolEditor { - return DeveloperToolEditor( - id = "target", - title = textConverterContext.targetTitle, - editorMode = INPUT_OUTPUT, - parentDisposable = parentDisposable, - configuration = configuration, - context = context, - project = project, - textProperty = targetText, - diffSupport = textConverterContext.diffSupport?.let { diffSupport -> - DeveloperToolEditor.DiffSupport( - title = diffSupport.title, - secondTitle = textConverterContext.sourceTitle, - secondText = { sourceText.get() }, - ) - } - ).apply { - onFocusGained { - lastActiveInput.set(TARGET) - } - this.onTextChangeFromUi { text -> - if (liveConversion.get()) { - lastActiveInput.set(TARGET) - doToSource(text) - } - } - } - } - - private fun liveTransformToLastActiveInput() { - if (liveConversion.get()) { - // Trigger a text change. So if the text was changed in manual mode, it - // will now be converted once during the switch to live mode. - when (lastActiveInput.get()) { - SOURCE -> transformToTarget() - TARGET -> transformToSource() - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - enum class ActiveInput { - - SOURCE, - TARGET - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - data class TextConverterContext( - val convertActionTitle: String, - val revertActionTitle: String, - val sourceTitle: String, - val targetTitle: String, - val sourceErrorHolder: ErrorHolder? = null, - val targetErrorHolder: ErrorHolder? = null, - val diffSupport: DiffSupport? = null, - val defaultSourceText: String = "", - val defaultTargetText: String = "" - ) - - data class DiffSupport( - val title: String - ) - - // -- Companion Object -------------------------------------------------------------------------------------------- // -} - diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/TextEscapers.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/TextEscapers.kt deleted file mode 100644 index 6211088e..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/TextEscapers.kt +++ /dev/null @@ -1,233 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.TextConverter.TextConverterContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import org.apache.commons.text.StringEscapeUtils - -// -- Properties ---------------------------------------------------------------------------------------------------- // -// -- Exposed Methods ----------------------------------------------------------------------------------------------- // - -internal fun createEscapeUnescapeContext(title: String) = TextConverterContext( - convertActionTitle = "Escape", - revertActionTitle = "Unescape", - sourceTitle = "Unescaped", - targetTitle = "Escaped", - diffSupport = TextConverter.DiffSupport( - title = title - ) -) - -// -- Private Methods ----------------------------------------------------------------------------------------------- // -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class HtmlEntitiesEscape( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = createEscapeUnescapeContext("HTML Entities Escape/Unescape"), - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(StringEscapeUtils.escapeHtml4(text)) - } - - override fun toSource(text: String) { - sourceText.set(StringEscapeUtils.unescapeHtml4(text)) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "HTML Entities Escaping", - groupedMenuTitle = "HTML Entities", - contentTitle = "HTML Entities Escape/Unescape", - description = DeveloperUiToolPresentation.contextHelp("This tool will use StringEscapeUtils.escapeHtml4(text) and StringEscapeUtils.unescapeHtml4(text) from the 'Apache Commons Text' library.") - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> HtmlEntitiesEscape) = - { configuration -> HtmlEntitiesEscape(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class JavaStringEscape( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = createEscapeUnescapeContext("Java String Escape/Unescape"), - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(StringEscapeUtils.escapeJava(text)) - } - - override fun toSource(text: String) { - sourceText.set(StringEscapeUtils.unescapeJava(text)) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Java String Escaping", - groupedMenuTitle = "Java String", - contentTitle = "Java String Escape/Unescape", - description = DeveloperUiToolPresentation.contextHelp("This tool will use StringEscapeUtils.escapeJava(text) and StringEscapeUtils.unescapeJava(text) from the 'Apache Commons Text' library.") - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> JavaStringEscape) = - { configuration -> JavaStringEscape(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class JsonTextEscape( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = createEscapeUnescapeContext("JSON Text Escape/Unescape"), - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(StringEscapeUtils.escapeJson(text)) - } - - override fun toSource(text: String) { - sourceText.set(StringEscapeUtils.unescapeJson(text)) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "JSON Text Escaping", - groupedMenuTitle = "JSON Text", - contentTitle = "JSON Text Escape/Unescape", - description = DeveloperUiToolPresentation.contextHelp("This tool will use StringEscapeUtils.escapeJson(text) and StringEscapeUtils.unescapeJson(text) from the 'Apache Commons Text' library.") - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> JsonTextEscape) = - { configuration -> JsonTextEscape(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class CsvTextEscape( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = createEscapeUnescapeContext("CSV Text Escape/Unescape"), - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(StringEscapeUtils.escapeCsv(text)) - } - - override fun toSource(text: String) { - sourceText.set(StringEscapeUtils.unescapeCsv(text)) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "CSV Text Escaping", - groupedMenuTitle = "CSV Text", - contentTitle = "CSV Text Escape/Unescape", - description = DeveloperUiToolPresentation.contextHelp("This tool will use StringEscapeUtils.escapeCsv(text) and StringEscapeUtils.unescapeCsv(text) from the 'Apache Commons Text' library.") - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> CsvTextEscape) = - { configuration -> CsvTextEscape(configuration, parentDisposable, context, project) } - } -} - -// -- Type ---------------------------------------------------------------------------------------------------------- // - -internal class XmlTextEscape( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - context: DeveloperUiToolContext, - project: Project? -) : - TextConverter( - textConverterContext = createEscapeUnescapeContext("XML Text Escape/Unescape"), - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - - override fun toTarget(text: String) { - targetText.set(StringEscapeUtils.escapeXml11(text)) - } - - override fun toSource(text: String) { - sourceText.set(StringEscapeUtils.unescapeXml(text)) - } - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "XML Text Escaping", - contentTitle = "XML Text Escape/Unescape", - description = DeveloperUiToolPresentation.contextHelp("This tool will use StringEscapeUtils.escapeXml11(text) and StringEscapeUtils.unescapeXml(text) from the 'Apache Commons Text' library.") - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> XmlTextEscape) = - { configuration -> XmlTextEscape(configuration, parentDisposable, context, project) } - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/BaseConverter.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/BaseConverter.kt deleted file mode 100644 index e471879f..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/BaseConverter.kt +++ /dev/null @@ -1,167 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter - -import com.intellij.openapi.Disposable -import com.intellij.ui.components.JBTextField -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.Cell -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.not -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import java.math.BigInteger - -class BaseConverter( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : UnitConverter(parentDisposable, "Base") { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val baseTwoInput = configuration.register("${CONFIGURATION_KEY_PREFIX}baseTwoInput", "0", INPUT, EXAMPLE_BASE_TWO_INPUT) - private val showOnlyCommonBases = configuration.register("${CONFIGURATION_KEY_PREFIX}showOnlyCommonBases", true, CONFIGURATION) - - private lateinit var baseProperties: Map> - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - @Suppress("UnstableApiUsage") - override fun Panel.buildUi() { - baseProperties = bases.map { (base, title) -> - val property = if (base == 2) baseTwoInput else ValueProperty("0") - row { - lateinit var textField: JBTextField - val textFieldCell = textField() - .validate(base) - .label("$title ($base):") - .bindText(property) - .whenTextChangedFromUi { convertFromCommonBase(base, textField) } - .resizableColumn() - .align(Align.FILL) - .gap(RightGap.SMALL) - if (!commonBases.contains(base)) { - textFieldCell.visibleIf(showOnlyCommonBases.not()) - } - textField = textFieldCell.component - }.layout(RowLayout.PARENT_GRID) - - base to property - }.toMap() - } - - override fun Panel.buildSettingsUi() { - collapsibleGroup("Settings") { - row { - checkBox("Show only common bases") - .bindSelected(showOnlyCommonBases) - } - }.topGap(TopGap.NONE) - } - - override fun sync() { - convertFromCommonBase(2, null) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun convertFromCommonBase(inputBase: Int, textField: JBTextField?) { - if (textField != null && validate().any { it.component == textField }) { - return - } - - val inputProperty = baseProperties[inputBase] ?: throw IllegalArgumentException("Unknown base: $inputBase") - val input = inputProperty.get().parse(inputBase) ?: return - - baseProperties.filter { it.value != inputProperty }.forEach { (base, property) -> - property.set(input.toString(base).uppercase()) - } - } - - @Suppress("UnstableApiUsage") - private fun Cell.validate(base: Int) = - this.apply { - validationInfo { - if (!this@validate.component.isEnabled) { - return@validationInfo null - } - try { - this@validate.component.text.parse(base) - ?: return@validationInfo error("Please enter a number") - return@validationInfo null - } catch (_: Exception) { - return@validationInfo error("Please enter a valid number") - } - } - } - - private fun String.parse(base: Int): BigInteger? { - if (this.isBlank()) { - return null - } - - val maxAllowedChar = validChars[base - 1] - val isValid = this.uppercase().all { it in '0'..maxAllowedChar } - if (!isValid) { - throw IllegalArgumentException("Invalid input for base $base") - } - - return BigInteger(this, base) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val CONFIGURATION_KEY_PREFIX = "baseConverter_" - private const val EXAMPLE_BASE_TWO_INPUT = "10101010" - - private val validChars = ('0'..'9') + ('A'..'Z') - - private val commonBases = setOf(2, 8, 10, 16) - private val bases = linkedMapOf( - 2 to "Binary", - 3 to "Ternary", - 4 to "Quaternary", - 5 to "Quinary", - 6 to "Senary", - 7 to "Septenary", - 8 to "Octal", - 9 to "Nonary", - 10 to "Decimal", - 11 to "Undecimal", - 12 to "Duodecimal", - 13 to "Tridecimal", - 14 to "Tetradecimal", - 15 to "Pentadecimal", - 16 to "Hexadecimal", - 17 to "Heptadecimal", - 18 to "Octodecimal", - 19 to "Enneadecimal", - 20 to "Vigesimal", - 21 to "Unvigesimal", - 22 to "Duovigesimal", - 23 to "Trivigesimal", - 24 to "Tetravigesimal", - 25 to "Pentavigesimal", - 26 to "Hexavigesimal", - 27 to "Septemvigesimal", - 28 to "Octovigesimal", - 29 to "Ennevigesimal", - 30 to "Trigesimal", - 31 to "Untrigesimal", - 32 to "Duotrigesimal", - 33 to "Tretrigesimal", - 34 to "Tetratrigesimal", - 35 to "Pentatrigesimal", - 36 to "Hexatrigesimal" - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/DataSizeConverter.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/DataSizeConverter.kt deleted file mode 100644 index 6fd62476..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/DataSizeConverter.kt +++ /dev/null @@ -1,152 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter - -import com.intellij.openapi.Disposable -import com.intellij.ui.components.JBTextField -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.Cell -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.whenStateChangedFromUi -import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateBigDecimalValue -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.DataUnit -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.NumberSystem -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.bitDataUnit -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.dataUnits -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import java.math.BigDecimal -import java.math.BigDecimal.ZERO - -class DataSizeConverter( - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : MathContextUnitConverter(CONFIGURATION_KEY_PREFIX, configuration, parentDisposable, "Data Size") { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val bitDataSizeValue = configuration.register("${CONFIGURATION_KEY_PREFIX}bitDataSizeValue", ZERO, INPUT, DEFAULT_BIT_DATA_SIZE_VALUE) - private val showLargeDataUnits = configuration.register("${CONFIGURATION_KEY_PREFIX}showLargeDataUnits", DEFAULT_SHOW_LARGE_DATA_UNITS) - - private val dataSizeProperties: List = createDataProperties() - private val bitDataSizeProperty = dataSizeProperties.first { it.dataUnit == bitDataUnit } - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - @Suppress("UnstableApiUsage") - override fun Panel.buildUi() { - NumberSystem.entries.forEach { numberSystem -> - group(numberSystem.title, true) { - dataSizeProperties - .filter { it.dataUnit.numberSystem == numberSystem } - .forEach { transferRateDataUnitProperty -> - row { - val formattedLabel = label(transferRateDataUnitProperty.inputTitle) - .gap(RightGap.SMALL) - lateinit var formattedFieldTextField: Cell - formattedFieldTextField = textField() - .bindText(transferRateDataUnitProperty.formattedValue) - .validateBigDecimalValue(ZERO, mathContext) { it.parseBigDecimal() } - .resizableColumn() - .align(Align.FILL) - .whenTextChangedFromUi { - convertByInputFieldChange(formattedFieldTextField.component, transferRateDataUnitProperty) - } - - if (transferRateDataUnitProperty.dataUnit.isLarge) { - formattedLabel.visibleIf(showLargeDataUnits) - formattedFieldTextField.visibleIf(showLargeDataUnits) - } - }.layout(RowLayout.PARENT_GRID) - } - }.bottomGap(BottomGap.NONE).topGap(TopGap.NONE) - } - } - - private fun convertByInputFieldChange( - inputFieldComponent: JBTextField?, - inputDataSizeProperty: DataSizeProperty - ) { - if (inputFieldComponent != null && validate().any { it.component == inputFieldComponent }) { - return - } - - if (inputDataSizeProperty != bitDataSizeProperty) { - val inputValue = inputDataSizeProperty.formattedValue.get().parseBigDecimal() - bitDataSizeValue.set(inputDataSizeProperty.dataUnit.toBits(inputValue, mathContext)) - } - else { - bitDataSizeValue.set(bitDataSizeProperty.formattedValue.get().parseBigDecimal()) - } - - dataSizeProperties - .filter { it != inputDataSizeProperty } - .forEach { it.setFromBits(bitDataSizeValue.get(), this) } - } - - override fun doSync() { - // During a reset, only the `bitDataSizeValue` will be changed but - // `convertByInputFieldChange` would overwrite the value with the old - // formatted value. - bitDataSizeProperty.formattedValue.set(bitDataSizeValue.get().toFormatted()) - - convertByInputFieldChange(null, bitDataSizeProperty) - } - - @Suppress("UnstableApiUsage") - override fun Panel.buildAdditionalSettingsUi() { - row { - checkBox("Show large data units") - .bindSelected(showLargeDataUnits) - .whenStateChangedFromUi { sync() } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun createDataProperties() = dataUnits.map { - if (it == bitDataUnit) { - DataSizeProperty(it, bitDataSizeValue).apply { formattedValue.set(bitDataSizeValue.get().toFormatted()) } - } - else { - DataSizeProperty(it, null) - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class DataSizeProperty( - val dataUnit: DataUnit, - val rawValueReference: ValueProperty? = null - ) { - - val formattedValue: ValueProperty = ValueProperty("0") - var inputTitle: String = "${dataUnit.name}:" - - fun setFromBits( - bits: BigDecimal, - unitConverter: MathContextUnitConverter - ) { - val result = dataUnit.fromBits(bits, unitConverter.mathContext) - formattedValue.set(with(unitConverter) { result.toFormatted() }) - rawValueReference?.set(result) - } - - override fun toString(): String = dataUnit.name - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val CONFIGURATION_KEY_PREFIX = "dataSizeConverter_" - private val DEFAULT_BIT_DATA_SIZE_VALUE = BigDecimal(1073740000) - private const val DEFAULT_SHOW_LARGE_DATA_UNITS = false - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/DataUnit.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/DataUnit.kt deleted file mode 100644 index ba9187b2..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/DataUnit.kt +++ /dev/null @@ -1,176 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter - -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.BaseDataUnit.BIT -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.BaseDataUnit.BYTE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.NumberSystem.BASIC -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.NumberSystem.BINARY -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.DataUnits.NumberSystem.DECIMAL -import java.math.BigDecimal -import java.math.MathContext - -internal object DataUnits { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val bigDecimalEight = BigDecimal.valueOf(8) - - val bitDataUnit = DataUnit( - name = "Bit", - abbreviation = "b", - isLarge = false, - baseDataUnit = BIT, - numberSystem = BASIC, - exponent = 0 - ) - - val dataUnits: List = listOf( - listOf( - bitDataUnit, - DataUnit( - name = "Byte", - abbreviation = "B", - isLarge = false, - baseDataUnit = BIT, - numberSystem = BASIC, - exponent = 3 - ) - ), - createDataUnits( - decimalPrefix = "Kilo", - binaryPrefix = "Kibi", - decimalAbbreviationFirstLetter = 'k', - binaryAbbreviationFirstLetter = 'K', - decimalExponent = 3, - binaryExponent = 10, - isLarge = false - ), - createDataUnits( - decimalPrefix = "Mega", - binaryPrefix = "Mebi", - decimalAbbreviationFirstLetter = 'M', - binaryAbbreviationFirstLetter = 'M', - decimalExponent = 6, - binaryExponent = 20, - isLarge = false - ), - createDataUnits( - decimalPrefix = "Giga", - binaryPrefix = "Gibi", - decimalAbbreviationFirstLetter = 'G', - binaryAbbreviationFirstLetter = 'G', - decimalExponent = 9, - binaryExponent = 30, - isLarge = false - ), - createDataUnits( - decimalPrefix = "Tera", - binaryPrefix = "Tebi", - decimalAbbreviationFirstLetter = 'T', - binaryAbbreviationFirstLetter = 'T', - decimalExponent = 12, - binaryExponent = 40, - isLarge = false - ), - createDataUnits( - decimalPrefix = "Peta", - binaryPrefix = "Pebi", - decimalAbbreviationFirstLetter = 'P', - binaryAbbreviationFirstLetter = 'P', - decimalExponent = 15, - binaryExponent = 50, - isLarge = true - ), - createDataUnits( - decimalPrefix = "Exa", - binaryPrefix = "Exbi", - decimalAbbreviationFirstLetter = 'E', - binaryAbbreviationFirstLetter = 'E', - decimalExponent = 18, - binaryExponent = 60, - isLarge = true - ), - createDataUnits( - decimalPrefix = "Zetta", - binaryPrefix = "Zebi", - decimalAbbreviationFirstLetter = 'Z', - binaryAbbreviationFirstLetter = 'Z', - decimalExponent = 21, - binaryExponent = 70, - isLarge = true - ), - createDataUnits( - decimalPrefix = "Yotta", - binaryPrefix = "Yobi", - decimalAbbreviationFirstLetter = 'Y', - binaryAbbreviationFirstLetter = 'Y', - decimalExponent = 24, - binaryExponent = 80, - isLarge = true - ) - ).flatten() - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun createDataUnits( - decimalPrefix: String, - binaryPrefix: String, - decimalAbbreviationFirstLetter: Char, - binaryAbbreviationFirstLetter: Char, - decimalExponent: Int, - binaryExponent: Int, - isLarge: Boolean - ): List = listOf( - DataUnit("${decimalPrefix}bit", "${decimalAbbreviationFirstLetter}b", isLarge, BIT, DECIMAL, decimalExponent), - DataUnit("${binaryPrefix}bit", "${decimalAbbreviationFirstLetter}ib", isLarge, BIT, BINARY, binaryExponent), - DataUnit("${decimalPrefix}byte", "${binaryAbbreviationFirstLetter}B", isLarge, BYTE, DECIMAL, decimalExponent), - DataUnit("${binaryPrefix}byte", "${binaryAbbreviationFirstLetter}iB", isLarge, BYTE, BINARY, binaryExponent) - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - enum class NumberSystem(val title: String, val base: BigDecimal) { - - BASIC("Basic", BigDecimal.valueOf(2)), - BINARY("Binary", BigDecimal.valueOf(2)), - DECIMAL("Decimal", BigDecimal.valueOf(10)) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - enum class BaseDataUnit { - - BIT, - BYTE, - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - open class DataUnit( - val name: String, - val abbreviation: String, - val isLarge: Boolean, - val baseDataUnit: BaseDataUnit, - val numberSystem: NumberSystem, - private val exponent: Int - ) { - - fun toBits(value: BigDecimal, mathContext: MathContext): BigDecimal { - val conversationFactor = when (baseDataUnit) { - BIT -> numberSystem.base.pow(exponent, mathContext) - BYTE -> numberSystem.base.pow(exponent, mathContext).multiply(bigDecimalEight) - } - return value.multiply(conversationFactor, mathContext) - } - - fun fromBits(value: BigDecimal, mathContext: MathContext): BigDecimal { - val conversationFactor = when (baseDataUnit) { - BIT -> numberSystem.base.pow(exponent, mathContext) - BYTE -> numberSystem.base.pow(exponent, mathContext).multiply(bigDecimalEight) - } - return value.divide(conversationFactor, mathContext) - } - - override fun toString(): String = name - } -} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/MathContextUnitConverter.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/MathContextUnitConverter.kt deleted file mode 100644 index ec3c776c..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/MathContextUnitConverter.kt +++ /dev/null @@ -1,168 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter - -import com.intellij.openapi.Disposable -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.COLUMNS_MEDIUM -import com.intellij.ui.dsl.builder.COLUMNS_TINY -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.columns -import com.intellij.ui.dsl.builder.whenItemSelectedFromUi -import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer.Companion.ALL_AVAILABLE_LOCALES -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bindIntTextImproved -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateLongValue -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import java.math.BigDecimal -import java.math.MathContext -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols -import java.text.ParsePosition -import java.util.* - -abstract class MathContextUnitConverter( - configurationKeyPrefix: String, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - title: String -): UnitConverter(parentDisposable, title) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var parsingLocale = configuration.register("${configurationKeyPrefix}parsingLocale", DEFAULT_PARSING_LOCALE) - private val roundingMode = configuration.register("${configurationKeyPrefix}roundingMode", DEFAULT_ROUNDING_MODE) - private val decimalPlaces = configuration.register("${configurationKeyPrefix}decimalPlaces", DEFAULT_DECIMAL_PLACES) - private val precision = configuration.register("${configurationKeyPrefix}precision", DEFAULT_PRECISION) - - private val parsingDecimalSeparatorInfo = ValueProperty("") - var mathContext = createMathContext() - private set - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - open fun Panel.buildAdditionalSettingsUi() { - // Override if needed - } - - fun String.parseBigDecimal(): BigDecimal { - val decimalFormat = DecimalFormat().apply { - decimalFormatSymbols = DecimalFormatSymbols.getInstance(parsingLocale.get().locale) - } - // The `DecimalFormat` is non-strict. So `12foo3` would return `123`. - val parsePosition = ParsePosition(0) - val parsedNumber: Number? = decimalFormat.parse(this, parsePosition) - if (parsePosition.index != this.length || parsedNumber == null) { - throw NumberFormatException("Invalid number format: $this") - } - return BigDecimal(parsedNumber.toString(), mathContext) - } - - fun BigDecimal.toFormatted() = this.stripTrailingZeros() - .setScale(decimalPlaces.get(), roundingMode.get().javaMathRoundingMode) - .toPlainString() - .let { - // Remove trailing zeros - if (it.contains(".")) { - val trimmed = it.trimEnd('0') - if (trimmed.endsWith('.')) trimmed.dropLast(1) else trimmed - } - else { - it - } - }.replace(".", getDecimalSeparator()) - - override fun sync() { - mathContext = createMathContext() - - val decimalSeparator = getDecimalSeparator() - val postfix = when (decimalSeparator) { - "." -> " (dot)" - "," -> " (comma)" - else -> "" - } - parsingDecimalSeparatorInfo.set("Decimal separator: $decimalSeparator$postfix") - - doSync() - } - - abstract fun doSync() - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun getDecimalSeparator(): String = - DecimalFormatSymbols.getInstance(parsingLocale.get().locale).decimalSeparator.toString() - - @Suppress("UnstableApiUsage") - override fun Panel.buildSettingsUi() { - collapsibleGroup("Settings") { - buildAdditionalSettingsUi() - row { - comboBox(ALL_AVAILABLE_LOCALES) - .label("Locale for parsing:") - .bindItem(parsingLocale) - .columns(COLUMNS_MEDIUM) - .whenItemSelectedFromUi { sync() } - }.layout(RowLayout.PARENT_GRID).bottomGap(BottomGap.NONE) - row { - cell() - label("") - .bindText(parsingDecimalSeparatorInfo) - }.layout(RowLayout.PARENT_GRID).topGap(TopGap.NONE) - row { - textField() - .label("Decimal places:") - .bindIntTextImproved(decimalPlaces) - .validateLongValue(LongRange(1, 50)) - .columns(COLUMNS_TINY) - .whenTextChangedFromUi { sync() } - }.layout(RowLayout.PARENT_GRID) - row { - comboBox(RoundingMode.entries) - .label("Rounding mode:") - .bindItem(roundingMode) - .whenItemSelectedFromUi { sync() } - }.layout(RowLayout.PARENT_GRID) - row { - textField() - .label("Precision:") - .bindIntTextImproved(precision) - .validateLongValue(LongRange(1, 100)) - .columns(COLUMNS_TINY) - .whenTextChangedFromUi { sync() } - }.layout(RowLayout.PARENT_GRID) - }.topGap(TopGap.NONE) - } - - private fun createMathContext() = - MathContext(precision.get(), roundingMode.get().javaMathRoundingMode) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class RoundingMode(val title: String, val javaMathRoundingMode: java.math.RoundingMode) { - - DOWN("Down", java.math.RoundingMode.DOWN), - UP("Up", java.math.RoundingMode.UP), - CEILING("Ceiling", java.math.RoundingMode.CEILING), - FLOOR("Floor", java.math.RoundingMode.FLOOR), - HALF_UP("Half up", java.math.RoundingMode.HALF_UP), - HALF_DOWN("Half down", java.math.RoundingMode.HALF_DOWN), - HALF_EVEN("Half even", java.math.RoundingMode.HALF_EVEN); - - override fun toString(): String = title - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val DEFAULT_ROUNDING_MODE = RoundingMode.HALF_UP - private const val DEFAULT_DECIMAL_PLACES = 5 - private const val DEFAULT_PRECISION = 50 - private val DEFAULT_PARSING_LOCALE = LocaleContainer(Locale.getDefault()) - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/UnitsConverter.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/UnitsConverter.kt deleted file mode 100644 index ab378e65..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/unitconverter/UnitsConverter.kt +++ /dev/null @@ -1,142 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.ui.TabbedPaneWrapper -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.Panel -import com.intellij.util.ui.JBUI.Borders -import com.intellij.util.ui.components.BorderLayoutPanel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ScrollPaneBuilder -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.ResetListener -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import javax.swing.ScrollPaneConstants -import javax.swing.event.ChangeEvent -import javax.swing.event.ChangeListener - -class UnitsConverter( - private val configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : DeveloperUiTool(parentDisposable), ResetListener, ChangeListener { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var lastSelectedUnitConverterIndex by configuration.register("lastSelectedUnitConverterIndex", DEFAULT_LAST_SELECTED_UNIT_CONVERTER_INDEX) - - private val unitConverters: List = listOf( - TimeConverter(configuration, parentDisposable), - DataSizeConverter(configuration, parentDisposable), - TransferRateConverter(configuration, parentDisposable), - BaseConverter(configuration, parentDisposable) - ) - private var selectedUnitConverter: UnitConverter = unitConverters[0] - - private lateinit var unitConvertersTabbedPanel: TabbedPaneWrapper - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - wrapComponentInScrollPane = false - } - - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - unitConvertersTabbedPanel = TabbedPaneWrapper(parentDisposable).apply { - val tabBorder = Borders.emptyTop(12) - unitConverters.forEach { converter -> - val component = converter.createComponent().apply { - border = tabBorder - } - addTab( - converter.title, - ScrollPaneBuilder(component) - .horizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER) - .build() - ) - } - - val initialSelectedUnitConverterIndex = if (lastSelectedUnitConverterIndex < this.tabCount) { - lastSelectedUnitConverterIndex - } - else { - DEFAULT_LAST_SELECTED_UNIT_CONVERTER_INDEX - } - this.selectedIndex = initialSelectedUnitConverterIndex - } - row { - cell(BorderLayoutPanel().apply { addToCenter(unitConvertersTabbedPanel.component) }) - .resizableColumn() - .align(Align.FILL) - }.bottomGap(BottomGap.SMALL).resizableRow() - } - - override fun activated() { - selectedUnitConverter.activate() - - configuration.addResetListener(parentDisposable, this) - - unitConvertersTabbedPanel.addChangeListener(this) - } - - override fun deactivated() { - selectedUnitConverter.deactivate() - - configuration.removeResetListener(this) - - unitConvertersTabbedPanel.removeChangeListener(this) - } - - override fun stateChanged(e: ChangeEvent?) { - val oldSelectedUnitConverter = selectedUnitConverter - val newSelectedUnitConverter = unitConverters[unitConvertersTabbedPanel.selectedIndex] - if (oldSelectedUnitConverter != newSelectedUnitConverter) { - oldSelectedUnitConverter.deactivate() - newSelectedUnitConverter.activate() - selectedUnitConverter = newSelectedUnitConverter - lastSelectedUnitConverterIndex = unitConverters.indexOf(newSelectedUnitConverter) - } - } - - override fun configurationReset() { - sync() - } - - override fun afterBuildUi() { - sync() - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun sync() { - unitConverters.forEach { it.sync() } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Units Converter", - contentTitle = "Units Converter" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> UnitsConverter) = - { configuration -> UnitsConverter(configuration, parentDisposable) } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val DEFAULT_LAST_SELECTED_UNIT_CONVERTER_INDEX = 0 - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/LoremIpsumGenerator.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/LoremIpsumGenerator.kt deleted file mode 100644 index 0f6e2115..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/LoremIpsumGenerator.kt +++ /dev/null @@ -1,280 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator - -import ai.grazie.utils.capitalize -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.ComboBox -import com.intellij.ui.dsl.builder.COLUMNS_TINY -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.columns -import com.intellij.ui.layout.ComboBoxPredicate -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValidateMinIntValueSide.MAX -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValidateMinIntValueSide.MIN -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bindIntTextImproved -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateLongValue -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateMinMaxValueRelation -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.LoremIpsumGenerator.TextMode.BULLETS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.LoremIpsumGenerator.TextMode.PARAGRAPHS -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.LoremIpsumGenerator.TextMode.WORDS -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import java.security.SecureRandom -import kotlin.math.max -import kotlin.math.min - -class LoremIpsumGenerator( - project: Project?, - context: DeveloperUiToolContext, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : MultiLineTextGenerator( - generatedTextTitle = "Generated lorem ipsum", - configuration = configuration, - parentDisposable = parentDisposable, - context = context, - project = project - ) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var textMode = configuration.register("generatedTextKind", PARAGRAPHS) - private var numberOfValues = configuration.register("numberOfValues", 9) - private var minWordsInParagraph = configuration.register("minWordsInParagraph", DEFAULT_MIN_PARAGRAPH_WORDS) - private var maxWordsInParagraph = configuration.register("maxWordsInParagraph", DEFAULT_MAX_PARAGRAPH_WORDS) - private var minWordsInBullet = configuration.register("minWordsInBullet", DEFAULT_MIN_BULLET_WORDS) - private var maxWordsInBullet = configuration.register("maxWordsInBullet", DEFAULT_MAX_BULLET_WORDS) - private var startWithLoremIpsum = configuration.register("startWithLoremIpsum", true) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun generate(): String = when (textMode.get()) { - WORDS -> generateWords() - PARAGRAPHS -> generateParagraphs() - BULLETS -> generateBullets() - } - - override fun Panel.buildConfigurationUi() { - lateinit var textModeComboBox: ComboBox - row { - textField() - .bindIntTextImproved(numberOfValues) - .validateLongValue(LongRange(1, 999)) - .columns(COLUMNS_TINY) - .gap(RightGap.SMALL) - textModeComboBox = comboBox(TextMode.entries) - .bindItem(textMode) - .component - } - - row { - textField() - .label("Minimum words in paragraph:") - .bindIntTextImproved(minWordsInParagraph) - .validateLongValue(LongRange(1, 999)) - .columns(COLUMNS_TINY) - .validateMinMaxValueRelation(MIN) { maxWordsInParagraph.get() } - .gap(RightGap.SMALL) - textField() - .label("Maximum:") - .bindIntTextImproved(maxWordsInParagraph) - .validateLongValue(LongRange(1, 999)) - .columns(COLUMNS_TINY) - .validateMinMaxValueRelation(MAX) { minWordsInParagraph.get() } - }.visibleIf(ComboBoxPredicate(textModeComboBox) { it == PARAGRAPHS }) - - row { - textField() - .label("Minimum words in bullet:") - .bindIntTextImproved(minWordsInBullet) - .validateLongValue(LongRange(1, 999)) - .columns(COLUMNS_TINY) - .validateMinMaxValueRelation(MIN) { maxWordsInBullet.get() } - .gap(RightGap.SMALL) - textField() - .label("Maximum:") - .bindIntTextImproved(maxWordsInBullet) - .validateLongValue(LongRange(1, 999)) - .columns(COLUMNS_TINY) - .validateMinMaxValueRelation(MAX) { minWordsInBullet.get() } - }.visibleIf(ComboBoxPredicate(textModeComboBox) { it == BULLETS }) - - row { - checkBox("Start with iconic Lorem ipsum dolor sit amet…") - .bindSelected(startWithLoremIpsum) - } - } - - fun generateIconicText(atMostWords: Int, isSentence: Boolean): List { - val words = ICONIC_LOREM_IPSUM_SENTENCE.subList(0, min(ICONIC_LOREM_IPSUM_SENTENCE.size, atMostWords)).toMutableList() - - if (isSentence) { - val wordsSize = words.size - // Comma - if (wordsSize > ICONIC_LOREM_IPSUM_SENTENCE_COMMA_INDEX + 1) { - words[ICONIC_LOREM_IPSUM_SENTENCE_COMMA_INDEX] = "${words[ICONIC_LOREM_IPSUM_SENTENCE_COMMA_INDEX]}," - } - if (wordsSize >= 1) { - // Capitalize first character - words[0] = words[0].capitalize() - // Full stop - words[wordsSize - 1] = "${words[wordsSize - 1]}." - } - } - - return words - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun generateParagraphs() = IntRange(0, numberOfValues.get() - 1).joinToString(PARAGRAPH_SEPARATOR) { paragraphIndex -> - - val totalWordsInParagraph = SECURE_RANDOM.nextInt(minWordsInParagraph.get(), maxWordsInParagraph.get() + 1) - - val initialWords = if (paragraphIndex == 0 && startWithLoremIpsum.get()) { - generateIconicText(totalWordsInParagraph, true) - } - else { - emptyList() - } - - createSentences(initialWords, totalWordsInParagraph).joinToString(WORDS_SEPARATOR) - } - - private fun generateWords(): String { - val words = mutableListOf() - - if (startWithLoremIpsum.get()) { - words.addAll(generateIconicText(numberOfValues.get(), false)) - } - - if (words.size < numberOfValues.get()) { - words.addAll(getRandomWords(numberOfValues.get() - words.size)) - } - - return words.joinToString(WORDS_SEPARATOR) - } - - private fun generateBullets() = IntRange(0, numberOfValues.get() - 1).joinToString(BULLET_SEPARATOR) { bulletIndex -> - val words = mutableListOf() - - val totalWordsInBullet = SECURE_RANDOM.nextInt(minWordsInBullet.get(), maxWordsInBullet.get()) - - if (bulletIndex == 0 && startWithLoremIpsum.get()) { - words.addAll(generateIconicText(totalWordsInBullet, true)) - } - - // Avoid a single word sentence. - words.addAll(createSentences(words, totalWordsInBullet)) - - words[0] = "$BULLET_SYMBOL ${words[0]}" - - words.joinToString(WORDS_SEPARATOR) - } - - private fun getRandomWords(words: Int): List = IntRange(0, words - 1).asSequence() - .map { getRandomWord() } - .toList() - - private fun getRandomWord() = LOREM_IPSUM_WORDS[SECURE_RANDOM.nextInt(LOREM_IPSUM_WORDS.size)] - - private fun createSentence(words: List): List { - // Divide sentence in n-1 fragments (the last fragment does not get a comma). - val fragments = Math.floorDiv(words.size, TEXT_FRAGMENT_LENGTH) - 1 - val indiciesOfWordsWithCommas = IntRange(0, fragments - 1) - // Randomly decide with a 2/3 change to put comma in fragment - .filter { SECURE_RANDOM.nextInt(1, 4) != 3 } - // Randomly decide index after the first word - .map { fragmentIndex -> - val commaIndexInFragment = SECURE_RANDOM.nextInt(1, TEXT_FRAGMENT_LENGTH) - (TEXT_FRAGMENT_LENGTH * fragmentIndex) + commaIndexInFragment - } - .toSet() - - return words.mapIndexed { i: Int, rawWord: String -> - var word = rawWord - if (i == 0) { - word = word.capitalize() - } - if (i == words.size - 1) { - word = "$word." - } - if (indiciesOfWordsWithCommas.contains(i)) { - word = "$word," - } - word - } - } - - private fun createSentences(initialWords: List, totalWords: Int): List { - val words = initialWords.toMutableList() - - while (words.size < totalWords) { - val remainingWords = totalWords - words.size - // With the max we ensue that there are at least `MIN_SENTENCE_WORDS` words. - val minSentenceWords = max(min(MIN_SENTENCE_WORDS, remainingWords), MIN_SENTENCE_WORDS) - val maxSentenceWords = max(min(MAX_SENTENCE_WORDS, remainingWords), MIN_SENTENCE_WORDS) - val sentenceWords = if (minSentenceWords == maxSentenceWords) minSentenceWords else SECURE_RANDOM.nextInt(minSentenceWords, maxSentenceWords) - val elements = createSentence(getRandomWords(sentenceWords)) - words.addAll(elements) - } - - return words - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class TextMode(val title: String) { - - PARAGRAPHS("Paragraphs"), - WORDS("Words"), - BULLETS("Bullets"); - - override fun toString(): String = title - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Lorem Ipsum", - contentTitle = "Lorem Ipsum Generator" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> LoremIpsumGenerator) = { configuration -> - LoremIpsumGenerator(project, context, configuration, parentDisposable) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val SECURE_RANDOM = SecureRandom() - private val ICONIC_LOREM_IPSUM_SENTENCE = listOf("lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit") - private const val ICONIC_LOREM_IPSUM_SENTENCE_COMMA_INDEX = 4 - private val LOREM_IPSUM_WORDS: List by lazy { - LoremIpsumGenerator::class.java.getResource("/dev/turingcomplete/intellijdevelopertoolsplugin/lorem-ipsum.txt")!!.readText().lines() - } - private const val DEFAULT_MIN_PARAGRAPH_WORDS = 20 - private const val DEFAULT_MAX_PARAGRAPH_WORDS = 100 - private const val DEFAULT_MIN_BULLET_WORDS = 10 - private const val DEFAULT_MAX_BULLET_WORDS = 30 - private const val MIN_SENTENCE_WORDS = 3 - private const val MAX_SENTENCE_WORDS = 40 - private const val TEXT_FRAGMENT_LENGTH = 8 - private val PARAGRAPH_SEPARATOR = System.lineSeparator().repeat(2) - private const val WORDS_SEPARATOR = " " - private const val BULLET_SYMBOL = "-" - private val BULLET_SEPARATOR = System.lineSeparator().repeat(2) - } -} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/NanoIdGenerator.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/NanoIdGenerator.kt deleted file mode 100644 index 4a4eb32d..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/NanoIdGenerator.kt +++ /dev/null @@ -1,49 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator - -import com.aventrix.jnanoid.jnanoid.NanoIdUtils -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -internal class NanoIdGenerator( - project: Project?, - context: DeveloperUiToolContext, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : OneLineTextGenerator( - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun generate(): String = NanoIdUtils.randomNanoId() - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Nano ID", - contentTitle = "Nano ID Generator" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> NanoIdGenerator) = { configuration -> - NanoIdGenerator(project, context, configuration, parentDisposable) - } - } - - - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/MacAddressBasedUuidGenerator.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/MacAddressBasedUuidGenerator.kt deleted file mode 100644 index 0e9f5c6b..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/MacAddressBasedUuidGenerator.kt +++ /dev/null @@ -1,151 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid - -import com.fasterxml.uuid.EthernetAddress -import com.intellij.openapi.ui.ValidationInfo -import com.intellij.openapi.ui.asSequence -import com.intellij.ui.components.JBTextField -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.selected -import com.intellij.ui.dsl.builder.whenItemSelectedFromUi -import com.intellij.ui.layout.ComponentPredicate -import com.intellij.ui.layout.ValidationInfoBuilder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bind -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.MacAddressBasedUuidGenerator.MacAddressGenerationMode.INDIVIDUAL -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.MacAddressBasedUuidGenerator.MacAddressGenerationMode.LOCAL_INTERFACE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.MacAddressBasedUuidGenerator.MacAddressGenerationMode.RANDOM -import dev.turingcomplete.intellijdevelopertoolsplugin.common.toHexMacAddress -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import java.net.NetworkInterface -import java.net.SocketException - -abstract class MacAddressBasedUuidGenerator( - version: UuidVersion, - configuration: DeveloperToolConfiguration, - supportsBulkGeneration: Boolean -) : SpecificUuidGenerator(supportsBulkGeneration) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var macAddressGenerationMode = configuration.register( - "${version}MacAddressGenerationMode", - RANDOM - ) - private var localInterface by configuration.register( - "${version}LocalInterface", - "" - ) - private var individualMacAddress = configuration.register( - "${version}IndividualMacAddress", - "" - ) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - @Suppress("UnstableApiUsage") - override fun Panel.buildConfigurationUi(visible: ComponentPredicate) { - buttonsGroup("MAC address:") { - row { - radioButton("Generate random multicast MAC address") - .bind(macAddressGenerationMode, RANDOM) - } - - row { - val individualRadioButton = radioButton("Individual:") - .bind(macAddressGenerationMode, INDIVIDUAL) - .gap(RightGap.SMALL) - expandableTextField() - .bindText(individualMacAddress) - .validationInfo(validateIndividualMacAddress()) - .enabledIf(individualRadioButton.selected).component - } - - row { - val localMacAddresses = collectLocalMacAddresses() - visible(localMacAddresses.isNotEmpty()) - val useLocalInterface = radioButton("Local interface:") - .bind(macAddressGenerationMode, LOCAL_INTERFACE) - .gap(RightGap.SMALL) - comboBox(localMacAddresses) - .applyToComponent { - model.asSequence().firstOrNull { it.macAddress == localInterface }?.let { - selectedItem = it - } - } - .whenItemSelectedFromUi { localInterface = it.macAddress } - .enabledIf(useLocalInterface.selected).component - } - }.visibleIf(visible) - } - - fun getEthernetAddress(): EthernetAddress = when (macAddressGenerationMode.get()) { - RANDOM -> EthernetAddress.constructMulticastAddress() - INDIVIDUAL -> EthernetAddress.valueOf(individualMacAddress.get()) - LOCAL_INTERFACE -> EthernetAddress(localInterface) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun validateIndividualMacAddress(): ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = { - if (macAddressGenerationMode.get() == INDIVIDUAL && !MAC_ADDRESS_REGEX.matches(individualMacAddress.get())) { - INVALID_MAC_ADDRESS_VALIDATION_INFO - } - else { - null - } - } - - private fun collectLocalMacAddresses(): List { - return try { - NetworkInterface.getNetworkInterfaces().asSequence() - .filter { !it.isLoopback } - .filter { it.hardwareAddress != null } - .filter { it.hardwareAddress.size == 6 } - .groupBy { it.hardwareAddress.toHexMacAddress() } - .map { LocalInterface(it.key, it.value.joinToString("/") { networkInterface -> networkInterface.name }) } - .toList() - } catch (_: SocketException) { - emptyList() - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - enum class MacAddressGenerationMode { - - RANDOM, - INDIVIDUAL, - LOCAL_INTERFACE - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class LocalInterface(val macAddress: String, val title: String) { - - override fun toString(): String = "$title (${macAddress})" - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - if (javaClass != other?.javaClass) { - return false - } - - other as LocalInterface - - return macAddress == other.macAddress - } - - override fun hashCode(): Int = macAddress.hashCode() - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val MAC_ADDRESS_REGEX = Regex("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$") - private val INVALID_MAC_ADDRESS_VALIDATION_INFO = ValidationInfo("Must be a valid MAC address") - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/NamespaceAndNameBasedUuidGenerator.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/NamespaceAndNameBasedUuidGenerator.kt deleted file mode 100644 index 86392f63..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/uuid/NamespaceAndNameBasedUuidGenerator.kt +++ /dev/null @@ -1,127 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid - -import com.fasterxml.uuid.Generators -import com.intellij.openapi.Disposable -import com.intellij.openapi.ui.ValidationInfo -import com.intellij.ui.components.JBTextField -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.selected -import com.intellij.ui.dsl.builder.text -import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import com.intellij.ui.layout.ComponentPredicate -import com.intellij.ui.layout.ValidationInfoBuilder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bind -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.NamespaceAndNameBasedUuidGenerator.NamespaceMode.INDIVIDUAL -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.NamespaceAndNameBasedUuidGenerator.NamespaceMode.PREDEFINED -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import java.security.MessageDigest -import java.util.* - -abstract class NamespaceAndNameBasedUuidGenerator( - version: UuidVersion, - private val algorithm: MessageDigest, - configuration: DeveloperToolConfiguration, - private val parentDisposable: Disposable, - supportsBulkGeneration: Boolean -) : SpecificUuidGenerator(supportsBulkGeneration) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var namespaceMode = configuration.register( - "${version}NamespaceMode", - PREDEFINED - ) - private var predefinedNamespace = configuration.register( - "${version}PredefinedNamespace", - PredefinedNamespace.DNS - ) - private var individualNamespace = configuration.register( - "${version}IndividualNamespace", - "" - ) - private var name = configuration.register("${version}Name", "") - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - @Suppress("UnstableApiUsage") - override fun Panel.buildConfigurationUi(visible: ComponentPredicate) { - rowsRange { - buttonsGroup("Namespace:") { - row { - val usePredefined = radioButton("Predefined:") - .bind(namespaceMode, PREDEFINED) - .gap(RightGap.SMALL) - comboBox(PredefinedNamespace.entries) - .bindItem(predefinedNamespace) - .enabledIf(usePredefined.selected).component - } - - row { - val individualRadioButton = radioButton("Individual:") - .bind(namespaceMode, INDIVIDUAL) - .gap(RightGap.SMALL) - textField() - .text(individualNamespace.get()) - .validationInfo(validateIndividualNamespace()) - .whenTextChangedFromUi(parentDisposable) { individualNamespace.set(it) } - .enabledIf(individualRadioButton.selected).component - } - } - - row { - expandableTextField() - .label("Name:") - .bindText(name) - } - }.visibleIf(visible) - } - - private fun validateIndividualNamespace(): ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = { - if (namespaceMode.get() == INDIVIDUAL && !UUID_REGEX.matches(it.text)) { - ValidationInfo("Must be a valid UUID") - } - else { - null - } - } - - final override fun generate(): String { - val namespace: UUID = when (namespaceMode.get()) { - PREDEFINED -> predefinedNamespace.get().value - INDIVIDUAL -> UUID.fromString(individualNamespace.get()) - } - - return Generators.nameBasedGenerator(namespace, algorithm).generate(name.get()).toString() - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class NamespaceMode { - - PREDEFINED, - INDIVIDUAL - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class PredefinedNamespace(private val title: String, val value: UUID) { - - DNS("DNS", UUID.fromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")), - URL("URL", UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8")), - OID("OID", UUID.fromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8")), - X500DN("X.500 DN", UUID.fromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8")); - - override fun toString(): String = "$title ($value)" - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val UUID_REGEX = Regex("^[0-9a-fA-F]{8}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{4}\\b-[0-9a-fA-F]{12}\$") - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/ColorPicker.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/ColorPicker.kt deleted file mode 100644 index 0b0733dc..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/ColorPicker.kt +++ /dev/null @@ -1,275 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other - -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.DataKey -import com.intellij.openapi.actionSystem.DataProvider -import com.intellij.openapi.observable.properties.AtomicProperty -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.util.text.StringUtil -import com.intellij.ui.JBColor -import com.intellij.ui.colorpicker.ColorPickerBuilder -import com.intellij.ui.colorpicker.ColorPickerModel -import com.intellij.ui.colorpicker.MaterialGraphicalColorPipetteProvider -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.actionButton -import com.intellij.ui.dsl.builder.bindText -import com.intellij.util.ui.JBUI -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.CopyAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.NotBlankInputValidator -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.toJBColor -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import java.awt.Color -import java.util.* -import javax.swing.border.LineBorder -import kotlin.math.max -import kotlin.math.min - -class ColorPicker( - private val project: Project?, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : DeveloperUiTool(parentDisposable), DataProvider { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val selectedColor: ValueProperty = configuration.register("selectedColor", JBColor.MAGENTA.toJBColor(), INPUT) - - private val cssRgb = AtomicProperty("") - private val cssRgbWithAlpha = AtomicProperty("") - private val cssHex = AtomicProperty("") - private val cssHexWithAlpha = AtomicProperty("") - private val cssHls = AtomicProperty("") - private val cssHlsWithAlpha = AtomicProperty("") - - private lateinit var colorPickerModel: ColorPickerModel - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - setCssValues(selectedColor.get()) - - selectedColor.afterChangeConsumeEvent(parentDisposable) { - setCssValues(it.newValue) - if (it.id?.equals(COLOR_SELECTION_CHANGE_ID) != true) { - colorPickerModel.setColor(it.newValue) - } - } - } - - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - row { - val colorPicker = ColorPickerBuilder(showAlpha = true, showAlphaAsPercent = true) - .setOriginalColor(selectedColor.get()) - .withFocus() - .addSaturationBrightnessComponent() - .addColorAdjustPanel(MaterialGraphicalColorPipetteProvider()) - .addColorValuePanel() - .addColorListener({ color, _ -> selectedColor.set(color.toJBColor(), COLOR_SELECTION_CHANGE_ID) }, true) - .apply { colorPickerModel = this.model } - .build() - .apply { - content.apply { - border = LineBorder(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()) - } - } - cell(colorPicker.content).align(Align.FILL) - }.bottomGap(BottomGap.MEDIUM) - - group("CSS Colors") { - row { - label("") - .bindText(cssRgb) - .gap(RightGap.SMALL) - - actionButton(CopyAction(cssRgbDataKey), ColorPicker::class.java.name) - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - row { - label("") - .bindText(cssRgbWithAlpha) - .gap(RightGap.SMALL) - actionButton(CopyAction(cssRgbWithAlphaDataKey), ColorPicker::class.java.name) - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - row { - label("") - .bindText(cssHex) - .gap(RightGap.SMALL) - actionButton(CopyAction(cssHexDataKey), ColorPicker::class.java.name) - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - row { - label("") - .bindText(cssHexWithAlpha) - .gap(RightGap.SMALL) - actionButton(CopyAction(cssHexWithAlphaDataKey), ColorPicker::class.java.name) - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - row { - label("") - .bindText(cssHls) - .gap(RightGap.SMALL) - actionButton(CopyAction(cssHslDataKey), ColorPicker::class.java.name) - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - row { - label("") - .bindText(cssHlsWithAlpha) - .gap(RightGap.SMALL) - actionButton(CopyAction(cssHslWithAlphaDataKey), ColorPicker::class.java.name) - }.topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - row { - button("Parse CSS Color Value") { - val inputDialog = Messages.InputDialog( - project, - "CSS color value:", - PARSE_CSS_VALUE_DIALOG_TITLE, - null, - "", - NotBlankInputValidator() - ) - inputDialog.show() - inputDialog.inputString?.let { parseCssColorValue(it) }?.let { selectedColor.set(it) } - } - }.topGap(TopGap.NONE) - } - } - - private fun parseCssColorValue(inputString: String): JBColor? = try { - val color = org.silentsoft.csscolor4j.Color.valueOf(inputString) - JBColor( - Color(color.red, color.green, color.blue, (color.opacity * 255.0).toInt()), - Color(color.red, color.green, color.blue, (color.opacity * 255.0).toInt()) - ) - } - catch (e: IllegalArgumentException) { - Messages.showErrorDialog(project, e.message, PARSE_CSS_VALUE_DIALOG_TITLE) - null - } - catch (_: Exception) { - Messages.showErrorDialog(project, "Unable to parse input value.", PARSE_CSS_VALUE_DIALOG_TITLE) - null - } - - override fun getData(dataId: String): Any? = when { - cssRgbDataKey.`is`(dataId) -> StringUtil.stripHtml(cssRgb.get(), false) - cssRgbWithAlphaDataKey.`is`(dataId) -> StringUtil.stripHtml(cssRgbWithAlpha.get(), false) - cssHexDataKey.`is`(dataId) -> StringUtil.stripHtml(cssHex.get(), false) - cssHexWithAlphaDataKey.`is`(dataId) -> StringUtil.stripHtml(cssHexWithAlpha.get(), false) - cssHslDataKey.`is`(dataId) -> StringUtil.stripHtml(cssHls.get(), false) - cssHslWithAlphaDataKey.`is`(dataId) -> StringUtil.stripHtml(cssHlsWithAlpha.get(), false) - else -> null - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun setCssValues(color: Color) { - val red = color.red - val green = color.green - val blue = color.blue - val alpha = color.alpha - cssRgb.set("rgb($red, $green, $blue)") - cssRgbWithAlpha.set("rgba($red, $green, $blue, ${"%.2f".format(Locale.US, alpha / 255.0)})") - cssHex.set("${"#%02X%02X%02X".format(Locale.US, red, green, blue)}") - cssHexWithAlpha.set("${"#%02X%02X%02X%02X".format(Locale.US, red, green, blue, alpha)}") - val hsl = rgbToHsl(red, green, blue) - cssHls.set("${"hsl(%.2f, %.2f%%, %.2f%%)".format(Locale.US, hsl[0], hsl[1] * 100, hsl[2] * 100)}") - cssHlsWithAlpha.set( - "${ - "hsla(%.2f, %.2f%%, %.2f%%, %.2f)".format( - Locale.US, - hsl[0], - hsl[1] * 100, - hsl[2] * 100, - alpha / 255.0 - ) - }" - ) - } - - private fun rgbToHsl(red: Int, green: Int, blue: Int): FloatArray { - val r = red / 255f - val g = green / 255f - val b = blue / 255f - - val max = max(max(r.toDouble(), g.toDouble()), b.toDouble()).toFloat() - val min = min(min(r.toDouble(), g.toDouble()), b.toDouble()).toFloat() - - var h: Float - val s: Float - val l = (max + min) / 2 - - if (max == min) { - s = 0f - h = s - } - else { - val d = max - min - s = if (l > 0.5) d / (2 - max - min) else d / (max + min) - - h = when (max) { - r -> { - (g - b) / d + (if (g < b) 6 else 0) - } - - g -> { - (b - r) / d + 2 - } - - else -> { - (r - g) / d + 4 - } - } - - h /= 6f - } - - return floatArrayOf(h, s, l) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Color Picker", - contentTitle = "Color Picker" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> ColorPicker) = - { configuration -> ColorPicker(project, configuration, parentDisposable) } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val COLOR_SELECTION_CHANGE_ID = "colorSelection" - - private const val PARSE_CSS_VALUE_DIALOG_TITLE = "Parse CSS Color Value" - - private val cssRgbDataKey = DataKey.create("cssRgb") - private val cssRgbWithAlphaDataKey = DataKey.create("cssRgbWithAlpha") - private val cssHexDataKey = DataKey.create("cssHex") - private val cssHexWithAlphaDataKey = DataKey.create("cssHexWithAlpha") - private val cssHslDataKey = DataKey.create("cssHsl") - private val cssHslWithAlphaDataKey = DataKey.create("cssHslWithAlpha") - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/IntelliJInternals.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/IntelliJInternals.kt deleted file mode 100644 index 95823c0a..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/IntelliJInternals.kt +++ /dev/null @@ -1,309 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other - -import com.intellij.ide.BrowserUtil -import com.intellij.ide.plugins.IdeaPluginDescriptor -import com.intellij.ide.plugins.PluginManager -import com.intellij.ide.plugins.PluginManagerCore -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.DataProvider -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.extensions.PluginDescriptor -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.setEmptyState -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.ui.ScrollPaneFactory -import com.intellij.ui.TableSpeedSearch -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.table.JBTable -import com.intellij.ui.treeStructure.Tree -import com.intellij.util.lang.UrlClassLoader -import com.intellij.util.ui.ColumnInfo -import com.intellij.util.ui.ListTableModel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.CopyValuesAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.PluginCommonDataKeys -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils.simpleColumnInfo -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.setContextMenu -import dev.turingcomplete.intellijdevelopertoolsplugin.common.extension -import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import java.awt.Dimension -import java.net.URLClassLoader -import java.nio.file.Files -import java.nio.file.Paths -import javax.swing.ListSelectionModel -import javax.swing.tree.DefaultMutableTreeNode -import kotlin.streams.asSequence - -class IntelliJInternals( - parentDisposable: Disposable, - private val project: Project? -) : DeveloperUiTool(parentDisposable = parentDisposable), DataProvider { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private lateinit var pluginOverviewTable: JBTable - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - group("Plugins") { - row { - val pluginOverviewTableModel = ListTableModel(*pluginOverviewTableColumns).apply { - isSortable = true - } - pluginOverviewTable = JBTable(pluginOverviewTableModel).apply { - setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) - rowSelectionAllowed = true - columnSelectionAllowed = false - setContextMenu( - this::class.java.name, DefaultActionGroup( - CopyValuesAction(valueToString = { - val ideaPluginDescriptor = it as IdeaPluginDescriptor - "${ideaPluginDescriptor.name} (${ideaPluginDescriptor.pluginId.idString})" - }), - OpenPluginDirectory(), - OpenPluginDescriptor() - ) - ) - setEmptyState("No plugins") - TableSpeedSearch.installOn(this) - } - cell(ScrollPaneFactory.createScrollPane(pluginOverviewTable, false)) - .applyToComponent { preferredSize = Dimension(preferredSize.width, 350) } - .resizableColumn() - .align(Align.FILL) - }.bottomGap(BottomGap.NONE) - row { - button("Refresh") { populatePluginOverviewTableModel() } - }.topGap(TopGap.NONE) - - group("Find Plugin by Class Name") { - row { - val classNameTextField = textField() - .label("Class name:") - .resizableColumn() - .align(Align.FILL) - .component - button("Find") { - findPluginByClassName(classNameTextField.text.trim()) - } - } - } - } - - group("Plugin Class Loaders") { - row { - cell(ScrollPaneFactory.createScrollPane(Tree(traverseClassLoader(Thread.currentThread().contextClassLoader)), false)) - .align(Align.FILL) - .resizableColumn() - }.resizableRow() - - group("Find Class File Path by Class Name") { - row { - val classNameTextField = textField() - .label("Class name:") - .resizableColumn() - .align(Align.FILL) - .component - button("Find") { - findClassPathByClassName(classNameTextField.text.trim()) - } - } - } - } - } - - override fun afterBuildUi() { - populatePluginOverviewTableModel() - } - - override fun getData(dataId: String): Any? = when { - PluginCommonDataKeys.SELECTED_VALUES.`is`(dataId) -> { - val tableModel = pluginOverviewTable.model.uncheckedCastTo>() - val rowSorter = pluginOverviewTable.rowSorter - pluginOverviewTable.selectedRows.map { - tableModel.getRowValue(rowSorter.convertRowIndexToModel(it)) - }.toList() - } - - else -> null - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun populatePluginOverviewTableModel() { - pluginOverviewTable.model.uncheckedCastTo>().items = - PluginManager.getPlugins().sortedBy { it.name } - } - - private fun findPluginByClassName(className: String) { - val messageDialogTitle = "Find Plugin by Class Name" - try { - val plugin = PluginManager.getPluginByClass(Class.forName(className)) - if (plugin != null) { - Messages.showInfoMessage(project, "Class belongs to plugin: ${plugin.name} (ID: ${plugin.pluginId.idString}).", messageDialogTitle) - } - else { - Messages.showErrorDialog(project, "No plugin found for the given class name.", messageDialogTitle) - } - } catch (e: Exception) { - log.warn("Failed to find plugin", e) - Messages.showErrorDialog(project, "${e.message}: ${e::class.qualifiedName}", messageDialogTitle) - } - } - - private fun findClassPathByClassName(className: String) { - val messageDialogTitle = "Find Class File Path by Class Name" - try { - val aClass = IntelliJInternals::class.java.classLoader.loadClass(className) - val classFilePath = aClass.getResource('/' + aClass.getName().replace('.', '/') + ".class")?.toURI()?.path?.toString() - if (classFilePath != null) { - Messages.showInfoMessage(project, "Class file path: ${classFilePath}.", messageDialogTitle) - } - else { - Messages.showErrorDialog(project, "Unable to resolve the path of the class file for the given class name.", messageDialogTitle) - } - } catch (e: Exception) { - log.warn("Failed to find class file", e) - Messages.showErrorDialog(project, "${e.message}: ${e::class.qualifiedName}", messageDialogTitle) - } - } - - private fun traverseClassLoader(classLoader: ClassLoader): DefaultMutableTreeNode { - val classLoaderNode = DefaultMutableTreeNode("Class loader: ${classLoader::class.qualifiedName}") - - if (classLoader is URLClassLoader) { - classLoader.urLs.forEach { url -> - classLoaderNode.add(DefaultMutableTreeNode("File: ${Paths.get(url.toURI().path).fileName}")) - } - } - else if (classLoader is UrlClassLoader) { - classLoader.files.forEach { file -> - classLoaderNode.add(DefaultMutableTreeNode("File: ${file.fileName}")) - } - } - - val parentClassLoader = classLoader.parent - if (parentClassLoader != null) { - val parentClassLoaderNode = traverseClassLoader(parentClassLoader) - classLoaderNode.add(parentClassLoaderNode) - } - - return classLoaderNode - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class OpenPluginDirectory : DumbAwareAction("Open Plugin Directory") { - - override fun actionPerformed(e: AnActionEvent) { - val values: List = PluginCommonDataKeys.SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") - if (values.isEmpty()) { - return - } - - values.map { it as PluginDescriptor }.forEach { - BrowserUtil.browse(it.pluginPath) - } - } - } - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class OpenPluginDescriptor : DumbAwareAction("Open Plugin Descriptor") { - - override fun actionPerformed(e: AnActionEvent) { - val project = e.dataContext.getData(CommonDataKeys.PROJECT) ?: throw IllegalStateException("snh: Data missing") - val values: List = PluginCommonDataKeys.SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") - if (values.isEmpty()) { - return - } - - findPluginDescriptorFiles(values.map { it as IdeaPluginDescriptor }) { pluginDescriptorFiles -> - openProgramDescriptorFile(pluginDescriptorFiles, project) - } - } - - private fun findPluginDescriptorFiles( - pluginDescriptors: List, - callback: (List) -> Unit - ) { - ApplicationManager.getApplication().executeOnPooledThread { - val virtualFileManager = VirtualFileManager.getInstance() - val pluginDescriptorFiles = pluginDescriptors.flatMap { pluginDescriptor -> - Files.list(pluginDescriptor.pluginPath.resolve("lib")).use { files -> - files.asSequence() - .filter { Files.isRegularFile(it) && it.extension() == "jar" } - .mapNotNull { - virtualFileManager.findFileByUrl("jar://${it.toAbsolutePath()}!/${PluginManagerCore.PLUGIN_XML_PATH}") - } - .toList() - } - } - callback(pluginDescriptorFiles) - } - } - - private fun openProgramDescriptorFile(pluginDescriptorFiles: List, project: Project) { - invokeLater { - if (pluginDescriptorFiles.isEmpty()) { - Messages.showErrorDialog(project, "Unable to find plugin descriptor.", "Open Plugin Descriptor") - } - - val fileEditorManager = FileEditorManager.getInstance(project) - pluginDescriptorFiles.forEach { - val openEditor = fileEditorManager.openEditor(OpenFileDescriptor(project, it), true) - if (openEditor.isEmpty()) { - Messages.showErrorDialog(project, "Unable to open file '${it.name}' in editor.", "Open Plugin Descriptor") - } - } - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "IntelliJ Internals", - contentTitle = "IntelliJ Internals" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> IntelliJInternals) = { _ -> - IntelliJInternals(parentDisposable, project) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val log = logger() - - private val pluginOverviewTableColumns: Array> = arrayOf( - simpleColumnInfo("Name", { it.name }, { it.name }), - simpleColumnInfo("ID", { it.pluginId.idString }, { it.pluginId.idString }) - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/JsonSchemaValidator.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/JsonSchemaValidator.kt deleted file mode 100644 index 7ea48a7a..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/JsonSchemaValidator.kt +++ /dev/null @@ -1,256 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.intellij.icons.AllIcons -import com.intellij.json.JsonLanguage -import com.intellij.openapi.Disposable -import com.intellij.openapi.observable.properties.AtomicProperty -import com.intellij.openapi.observable.properties.ObservableMutableProperty -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.selected -import com.intellij.ui.layout.not -import com.networknt.schema.JsonSchema -import com.networknt.schema.JsonSchemaFactory -import com.networknt.schema.SpecVersionDetector -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor.EditorMode.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.PropertyComponentPredicate -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -class JsonSchemaValidator( - private val context: DeveloperUiToolContext, - private val configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - private val project: Project? -) : DeveloperUiTool(parentDisposable) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var liveValidation = configuration.register("liveValidation", true) - private val schemaText = configuration.register("schemaText", "", PropertyType.INPUT, EXAMPLE_SCHEMA) - private val dataText = configuration.register("dataText", "", PropertyType.INPUT, EXAMPLE_DATA) - - private val schemaEditor by lazy { this.createSchemaEditor() } - private val schemaErrorHolder = ErrorHolder() - private val dataEditor by lazy { this.createDataEditor() } - private val dataErrorHolder = ErrorHolder() - - private val validationState: ObservableMutableProperty = AtomicProperty(ValidationState.VALIDATED) - private val validationError: ObservableMutableProperty = AtomicProperty("") - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - liveValidation.afterChange { - if (it) { - validateSchema() - } - } - } - - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - row { - cell(schemaEditor.component).align(Align.FILL) - .validationOnApply(schemaEditor.bindValidator(schemaErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - }.resizableRow() - - row { - val liveValidationCheckBox = checkBox("Live validation") - .bindSelected(liveValidation) - .gap(RightGap.SMALL) - - button("Validate") { validateSchema() } - .enabledIf(liveValidationCheckBox.selected.not()) - .gap(RightGap.SMALL) - } - - row { - cell(dataEditor.component).align(Align.FILL) - .validationOnApply(dataEditor.bindValidator(dataErrorHolder.asValidation())) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - }.resizableRow() - - row { - icon(AllIcons.General.InspectionsOK).gap(RightGap.SMALL) - label("Data matches schema") - }.visibleIf(PropertyComponentPredicate(validationState, ValidationState.VALIDATED)) - - row { - icon(AllIcons.General.BalloonError).gap(RightGap.SMALL) - label("").bindText(validationError) - }.visibleIf(PropertyComponentPredicate(validationState, ValidationState.ERROR)) - - row { - icon(AllIcons.General.Warning).gap(RightGap.SMALL) - label("Invalid input") - }.visibleIf(PropertyComponentPredicate(validationState, ValidationState.INVALID_INPUT)) - } - - override fun afterBuildUi() { - if (liveValidation.get()) { - validateSchema() - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun validateSchema() { - schemaErrorHolder.clear() - dataErrorHolder.clear() - - val schema: JsonSchema? = try { - val schemaNode = objectMapper.readTree(schemaEditor.text) - JsonSchemaFactory.getInstance(SpecVersionDetector.detect(schemaNode)).getSchema(schemaNode) - } catch (e: Exception) { - schemaErrorHolder.add(e) - validationState.set(ValidationState.INVALID_INPUT) - null - } - - val dataNode: JsonNode? = try { - objectMapper.readTree(dataEditor.text) - } catch (e: Exception) { - dataErrorHolder.add(e) - validationState.set(ValidationState.INVALID_INPUT) - null - } - - // The `validate` in this class is not used as a validation mechanism. We - // make use of its text field error UI to display the `errorHolder`. - validate() - - if (schema != null && dataNode != null) { - val errors = schema.validate(dataNode) - if (errors.isEmpty()) { - validationState.set(ValidationState.VALIDATED) - } - else { - validationState.set(ValidationState.ERROR) - validationError.set( - """ - - Data does not match schema:
    - ${errors.joinToString(separator = "
    ") { "- $it" }} - - """.trimIndent() - ) - } - } - } - - private fun createSchemaEditor() = - DeveloperToolEditor( - id = "schema", - context = context, - configuration = configuration, - project = project, - title = "JSON schema", - editorMode = INPUT, - parentDisposable = parentDisposable, - initialLanguage = JsonLanguage.INSTANCE, - textProperty = schemaText, - ).apply { - onTextChangeFromUi { - if (liveValidation.get()) { - validateSchema() - } - } - } - - private fun createDataEditor() = - DeveloperToolEditor( - id = "data", - context = context, - configuration = configuration, - project = project, - title = "JSON data", - editorMode = INPUT, - parentDisposable = parentDisposable, - initialLanguage = JsonLanguage.INSTANCE, - textProperty = dataText, - ).apply { - onTextChangeFromUi { - if (liveValidation.get()) { - validateSchema() - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class ValidationState { - - VALIDATED, - ERROR, - INVALID_INPUT - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "JSON Schema", - contentTitle = "JSON Schema Validator" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> JsonSchemaValidator) = - { configuration -> JsonSchemaValidator(context, configuration, parentDisposable, project) } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val objectMapper = ObjectMapper() - - private val EXAMPLE_SCHEMA = """ -{ - "${'$'}id": "https://example.com/person.schema.json", - "${'$'}schema": "https://json-schema.org/draft/2020-12/schema", - "title": "Person", - "type": "object", - "properties": { - "firstName": { - "type": "string", - "description": "The person's first name." - }, - "lastName": { - "type": "string", - "description": "The person's last name." - }, - "age": { - "description": "Age in years which must be equal to or greater than zero.", - "type": "integer", - "minimum": 0 - } - } -} - """.trimIndent() - private val EXAMPLE_DATA = """ -{ - "firstName": "John", - "lastName": "Doe", - "age": 21 -} - """.trimIndent() - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/Notes.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/Notes.kt deleted file mode 100644 index 4ec6a49a..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/Notes.kt +++ /dev/null @@ -1,62 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.Panel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -class Notes( - private val context: DeveloperUiToolContext, - private val configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - private val project: Project? -) : DeveloperUiTool(parentDisposable = parentDisposable) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val text = configuration.register("test", "", INPUT) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - row { - cell( - DeveloperToolEditor( - id = "content", - context = context, - configuration = configuration, - project = project, - editorMode = DeveloperToolEditor.EditorMode.INPUT, - parentDisposable = parentDisposable, - textProperty = text - ).component - ).align(Align.FILL).resizableColumn() - }.resizableRow() - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Notes", - contentTitle = "Notes" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> Notes) = { configuration -> Notes(context, configuration, parentDisposable, project) } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/RegularExpressionMatcher.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/RegularExpressionMatcher.kt deleted file mode 100644 index 5c6fc605..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/RegularExpressionMatcher.kt +++ /dev/null @@ -1,493 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other - -import com.google.code.regexp.Pattern -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.DataProvider -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.editor.colors.EditorColors.SEARCH_RESULT_ATTRIBUTES -import com.intellij.openapi.editor.colors.EditorColors.TEXT_SEARCH_RESULT_ATTRIBUTES -import com.intellij.openapi.editor.colors.EditorColorsManager -import com.intellij.openapi.editor.markup.HighlighterLayer -import com.intellij.openapi.observable.util.whenFocusLost -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Splitter -import com.intellij.openapi.ui.setEmptyState -import com.intellij.openapi.util.TextRange -import com.intellij.ui.ColoredTableCellRenderer -import com.intellij.ui.ScrollPaneFactory -import com.intellij.ui.SimpleTextAttributes.GRAY_SMALL_ATTRIBUTES -import com.intellij.ui.SimpleTextAttributes.REGULAR_ATTRIBUTES -import com.intellij.ui.SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES -import com.intellij.ui.TableSpeedSearch -import com.intellij.ui.components.JBTabbedPane -import com.intellij.ui.components.JBViewport -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.LabelPosition -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.dsl.builder.whenTextChangedFromUi -import com.intellij.ui.table.JBTable -import com.intellij.util.ui.JBUI -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.CopyValuesAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.PluginCommonDataKeys -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.regex.RegexTextField -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.regex.SelectRegexOptionsAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.setContextMenu -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.message.UiToolsBundle -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other.RegularExpressionMatcher.MatchResultType.MATCH -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other.RegularExpressionMatcher.MatchResultType.NAMED_GROUP -import dev.turingcomplete.intellijdevelopertoolsplugin.common.getOrNull -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import java.awt.Dimension -import javax.swing.BorderFactory -import javax.swing.JTable -import javax.swing.ListSelectionModel -import javax.swing.event.ListSelectionListener -import javax.swing.table.AbstractTableModel - -class RegularExpressionMatcher( - private val context: DeveloperUiToolContext, - private val configuration: DeveloperToolConfiguration, - private val project: Project?, - parentDisposable: Disposable -) : DeveloperUiTool(parentDisposable) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var selectedRegexOptionFlag = configuration.register("regexOption", 0) - - private val regexPattern = configuration.register("regexText", "", INPUT, EXAMPLE_REGEX) - private val inputText = configuration.register("inputText", "", INPUT, EXAMPLE_INPUT_TEXT) - private val substitutionPattern = configuration.register("substitutionPattern", "", INPUT, EXAMPLE_SUBSTITUTION_PATTERN) - private val extractionPattern = configuration.register("extractionPattern", "", INPUT, EXAMPLE_EXTRACTION_PATTERN) - - private val substitutionResult = ValueProperty("") - private val extractionResult = ValueProperty("") - private lateinit var inputEditor: DeveloperToolEditor - - private val regexMatchingAttributes by lazy { EditorColorsManager.getInstance().globalScheme.getAttributes(SEARCH_RESULT_ATTRIBUTES) } - private val selectedMatchResultHighlightingAttributes by lazy { EditorColorsManager.getInstance().globalScheme.getAttributes(TEXT_SEARCH_RESULT_ATTRIBUTES) } - private lateinit var matchResultsTableModel: MatchResultsTableModel - - private val regexInputErrorHolder = ErrorHolder() - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - row { - cell( - Splitter(true, 0.7f).apply { - firstComponent = createInputComponent() - secondComponent = createResultComponent() - } - ).align(Align.FILL).resizableColumn() - }.resizableRow() - } - - override fun afterBuildUi() { - sync() - } - - override fun reset() { - sync() - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun createInputComponent() = panel { - row { - val regexTextField = RegexTextField(project, parentDisposable, regexPattern) - .onTextChangeFromUi { sync() } - cell(regexTextField) - .label(UiToolsBundle.message("regular-expression-matcher.regex-input"), LabelPosition.TOP) - .validationOnApply(regexInputErrorHolder.asValidation()) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .align(Align.FILL) - .resizableColumn() - .gap(RightGap.SMALL) - cell(SelectRegexOptionsAction.createActionButton(selectedRegexOptionFlag)) - }.topGap(TopGap.NONE) - - row { - inputEditor = DeveloperToolEditor( - id = "input", - context = context, - configuration = configuration, - project = project, - title = UiToolsBundle.message("regular-expression-matcher.text-input-title"), - editorMode = DeveloperToolEditor.EditorMode.INPUT, - parentDisposable = parentDisposable, - textProperty = inputText - ).onTextChangeFromUi { sync() } - cell(inputEditor.component).align(Align.FILL) - }.resizableRow().topGap(TopGap.SMALL) - } - - private fun createResultComponent() = panel { - row { - cell(JBTabbedPane().apply { - addTab( - UiToolsBundle.message("regular-expression-matcher.matches-title"), - createMatchesTableComponent() - ) - - addTab( - UiToolsBundle.message("regular-expression-matcher.substitution-title"), - createSubstitutionComponent() - ) - - addTab( - UiToolsBundle.message("regular-expression-matcher.extraction-title"), - createExtractionComponent() - ) - }).resizableColumn().align(Align.FILL) - }.resizableRow() - } - - private fun createMatchesTableComponent() = panel { - row { - val highlightSelectedMatchResults: (List) -> Unit = { textRanges -> - inputEditor.removeTextRangeHighlighters(SELECTED_MATCH_RESULT_HIGHLIGHTING_GROUP_ID) - textRanges.forEach { textRange -> - inputEditor.highlightTextRange( - textRange, - REGEX_MATCH_SELECTED_HIGHLIGHT_LAYER, - selectedMatchResultHighlightingAttributes, - SELECTED_MATCH_RESULT_HIGHLIGHTING_GROUP_ID - ) - } - } - matchResultsTableModel = MatchResultsTableModel() - val matchResultsTable = MatchResultsTable(matchResultsTableModel, highlightSelectedMatchResults).apply { - whenFocusLost(parentDisposable) { - inputEditor.removeTextRangeHighlighters(SELECTED_MATCH_RESULT_HIGHLIGHTING_GROUP_ID) - } - } - cell( - ScrollPaneFactory.createScrollPane(matchResultsTable).apply { - minimumSize = Dimension(minimumSize.width, 150) - preferredSize = Dimension(preferredSize.width, 150) - } - ).align(Align.FILL) - }.resizableRow() - } - - @Suppress("UnstableApiUsage") - private fun createSubstitutionComponent() = panel { - row { - expandableTextField() - .bindText(substitutionPattern) - .align(Align.FILL) - .resizableColumn() - .whenTextChangedFromUi { substitute() } - .gap(RightGap.SMALL) - contextHelp(UiToolsBundle.message("regular-expression-matcher.replace-pattern-context-help")) - } - - row { - cell( - DeveloperToolEditor( - id = "substitution-result", - context = context, - configuration = configuration, - project = project, - editorMode = DeveloperToolEditor.EditorMode.OUTPUT, - parentDisposable = parentDisposable, - textProperty = substitutionResult - ).component - ).align(Align.FILL) - }.resizableRow().topGap(TopGap.SMALL) - } - - @Suppress("UnstableApiUsage") - private fun createExtractionComponent() = panel { - row { - expandableTextField() - .bindText(extractionPattern) - .align(Align.FILL) - .resizableColumn() - .whenTextChangedFromUi { extract() } - .gap(RightGap.SMALL) - contextHelp(UiToolsBundle.message("regular-expression-matcher.replace-pattern-context-help")) - } - - row { - cell( - DeveloperToolEditor( - id = "extraction-result", - context = context, - configuration = configuration, - project = project, - editorMode = DeveloperToolEditor.EditorMode.OUTPUT, - parentDisposable = parentDisposable, - textProperty = extractionResult - ).component - ).align(Align.FILL) - }.resizableRow().topGap(TopGap.SMALL) - } - - private fun sync() { - match() - substitute() - extract() - } - - /** - * Kotlin and Java do not support the retrieval of all named groups yet: - * [KT-51671](https://youtrack.jetbrains.com/issue/KT-51671). Therefore, we - * are using Google's [com.google.code.regexp.Pattern] for now. - */ - private fun match() { - inputEditor.removeAllTextRangeHighlighters() - regexInputErrorHolder.clear() - matchResultsTableModel.setMatches(emptyList()) - - val regex = regexPattern.get() - if (regex.isEmpty()) { - return - } - - try { - val pattern = Pattern.compile(regex, selectedRegexOptionFlag.get()) - val matcher = pattern.matcher(inputEditor.text) - - val namedGroups = matcher.namedGroupsList() - val results = mutableListOf() - var i = 0 - while (matcher.find()) { - val textRange = TextRange(matcher.start(), matcher.end()) - inputEditor.highlightTextRange(textRange, REGEX_MATCH_HIGHLIGHT_LAYER, regexMatchingAttributes) - results.add(Match(i, "${i + 1}", textRange, matcher.group(), MATCH)) - if (namedGroups.isNotEmpty()) { - namedGroups[i].filter { it.value != null }.forEach { - results.add(Match(i, it.key, TextRange(matcher.start(it.key), matcher.end(it.key)), it.value, NAMED_GROUP)) - } - } - i++ - } - - matchResultsTableModel.setMatches(results) - } catch (e: Exception) { - regexInputErrorHolder.add(e) - } - - // The `validate` in this class is not used as a validation mechanism. We - // make use of its text field error UI to display the `regexInputErrorHolder`. - validate() - } - - private fun substitute() { - val regex = regexPattern.get() - if (regex.isEmpty()) { - return - } - - try { - val result = Regex(regex).replace(inputText.get()) { matchResult -> - substitutionPattern.get().replace(Regex("""\$(\d+)|\$\{(\d+)}""")) { groupMatch -> - val groupIndex = groupMatch.groups[1]?.value?.toInt() - ?: groupMatch.groups[2]?.value?.toInt() - ?: -1 - matchResult.groups.getOrNull(groupIndex)?.value ?: "\$$groupIndex" - } - } - substitutionResult.set(result) - } - catch (_: Exception) { - // An invalid pattern will be handled by `match`. - } - } - - private fun extract() { - val regex = regexPattern.get() - if (regex.isEmpty()) { - return - } - - try { - val result = Regex(regex).findAll(inputText.get()).map { matchResult -> - extractionPattern.get().replace(Regex("""\$(\d+)|\$\{(\d+)}""")) { groupMatch -> - val groupIndex = groupMatch.groups[1]?.value?.toInt() - ?: groupMatch.groups[2]?.value?.toInt() - ?: -1 - matchResult.groups.getOrNull(groupIndex)?.value ?: "\$$groupIndex" - } - }.joinToString(separator = "") - extractionResult.set(result) - } - catch (_: Exception) { - // An invalid pattern will be handled by `match`. - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class MatchResultsTable( - private val model: MatchResultsTableModel, - private val selectedMatchResultHighlight: (List) -> Unit - ) : JBTable(model), DataProvider { - - init { - columnModel.apply { - getColumn(0).preferredWidth = 150 - getColumn(1).preferredWidth = 300 - } - visibleRowCount = 4 - putClientProperty(JBViewport.FORCE_VISIBLE_ROW_COUNT_KEY, true) - autoResizeMode = AUTO_RESIZE_LAST_COLUMN - setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION) - rowSelectionAllowed = true - columnSelectionAllowed = false - setDefaultRenderer(Object::class.java, MatchResultsTableCellRenderer(model)) - selectionModel.addListSelectionListener(createSelectionListener()) - setContextMenu(this::class.java.name, DefaultActionGroup(CopyValuesAction())) - setEmptyState(UiToolsBundle.message("regular-expression-matcher.matches-no-matches")) - TableSpeedSearch.installOn(this) { value, cell -> - if (cell.column == 0 || cell.column == 1) value as String else null - } - } - - override fun getData(dataId: String): Any? = when { - PluginCommonDataKeys.SELECTED_VALUES.`is`(dataId) -> selectedRows.map { model.getValueAt(it, 1) as String }.toList() - else -> null - } - - @Suppress("UNCHECKED_CAST") - private fun createSelectionListener() = ListSelectionListener { e -> - if (!e.valueIsAdjusting) { - val selectedTextRanges = this@MatchResultsTable.selectedRows.map { (model.getValueAt(it, 0) as Pair).second }.toList() - selectedMatchResultHighlight(selectedTextRanges) - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private data class Match( - val groupIndex: Int, - val title: String, - val textRange: TextRange, - val value: String, - val matchResultType: MatchResultType - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class MatchResultsTableModel : AbstractTableModel() { - - private var matches = listOf() - - fun setMatches(matches: List) { - this.matches = matches - fireTableDataChanged() - } - - override fun getRowCount(): Int = matches.size - - override fun getColumnCount(): Int = 2 - - override fun getColumnName(column: Int): String = when (column) { - 0 -> UiToolsBundle.message("regular-expression-matcher.matches-group") - 1 -> UiToolsBundle.message("regular-expression-matcher.matches-value") - else -> error("Unknown column: $column") - } - - override fun getValueAt(row: Int, column: Int): Any = when(column) { - 0, 1 -> matches[row] - else -> error("Unknown column: $column") - } - - fun getRowMatchResultType(row: Int): MatchResultType = matches[row].matchResultType - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class MatchResultsTableCellRenderer(private val model: MatchResultsTableModel) : ColoredTableCellRenderer() { - - @Suppress("UNCHECKED_CAST") - override fun customizeCellRenderer(table: JTable, match: Any?, selected: Boolean, hasFocus: Boolean, row: Int, column: Int) { - check(match is Match) - - val matchResultType = model.getRowMatchResultType(row) - - when (column) { - 0 -> { - val prefix = when (matchResultType) { - MATCH -> UiToolsBundle.message("regular-expression-matcher.matches-match-prefix") - NAMED_GROUP -> UiToolsBundle.message("regular-expression-matcher.matches-group-prefix") - } - append("$prefix ", REGULAR_ATTRIBUTES) - append("${match.title} ", REGULAR_BOLD_ATTRIBUTES) - append("(${match.textRange.startOffset} to ${match.textRange.endOffset})", GRAY_SMALL_ATTRIBUTES) - - } - 1 -> append(match.value) - else -> error("Unknown column: $column") - } - - border = if (row > 0 && matchResultType == MATCH) { - BorderFactory.createCompoundBorder(matchResultAfterFirstMatchBorder, border) - } - else if (column == 0 && matchResultType == NAMED_GROUP) { - BorderFactory.createCompoundBorder(matchResultGroupBorder, border) - } - else { - border - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class MatchResultType { - - MATCH, - NAMED_GROUP - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = UiToolsBundle.message("regular-expression-matcher.menu-title"), - contentTitle = UiToolsBundle.message("regular-expression-matcher.content-title") - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> RegularExpressionMatcher) = - { configuration -> RegularExpressionMatcher(context, configuration, project, parentDisposable) } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val EXAMPLE_REGEX = "mid(?[a-zA-Z]+)" - private const val EXAMPLE_INPUT_TEXT = "aurora midsummer midnight earth" - private const val EXAMPLE_SUBSTITUTION_PATTERN = "deep \$1" - private const val EXAMPLE_EXTRACTION_PATTERN = "\${1}s " - - private const val REGEX_MATCH_HIGHLIGHT_LAYER = HighlighterLayer.SELECTION - 2 - private const val REGEX_MATCH_SELECTED_HIGHLIGHT_LAYER = HighlighterLayer.SELECTION - 1 - - private const val SELECTED_MATCH_RESULT_HIGHLIGHTING_GROUP_ID = "matchResultHighlighting" - - private val matchResultAfterFirstMatchBorder = JBUI.Borders.customLineTop(JBUI.CurrentTheme.CustomFrameDecorations.separatorForeground()) - private val matchResultGroupBorder = JBUI.Borders.emptyLeft(5) - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/ServerCertificates.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/ServerCertificates.kt deleted file mode 100644 index 1141a988..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/ServerCertificates.kt +++ /dev/null @@ -1,620 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other - -import ai.grazie.utils.capitalize -import com.intellij.icons.AllIcons -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.DataProvider -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.fileChooser.FileChooserFactory -import com.intellij.openapi.fileChooser.FileSaverDescriptor -import com.intellij.openapi.fileChooser.FileSaverDialog -import com.intellij.openapi.ide.CopyPasteManager -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.Task -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.popup.Balloon -import com.intellij.ui.HyperlinkAdapter -import com.intellij.ui.HyperlinkLabel -import com.intellij.ui.awt.RelativePoint -import com.intellij.ui.components.JBLabel -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.panel -import com.intellij.util.text.DateFormatUtil -import com.intellij.util.ui.components.BorderLayoutPanel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.AnActionOptionButton -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.DeveloperToolEditor -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.OkHttpClientUtils -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.OkHttpClientUtils.applyIntelliJProxySettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils.createPopup -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.copyable -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.message.UiToolsBundle -import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import okhttp3.OkHttpClient -import okhttp3.Request -import java.awt.datatransfer.StringSelection -import java.io.ByteArrayOutputStream -import java.math.BigInteger -import java.net.URI -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.StandardOpenOption -import java.security.KeyStore -import java.security.SecureRandom -import java.security.cert.Certificate -import java.security.cert.CertificateExpiredException -import java.security.cert.CertificateNotYetValidException -import java.security.cert.X509Certificate -import java.util.* -import javax.net.ssl.HostnameVerifier -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManager -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509TrustManager -import javax.security.auth.x500.X500Principal -import javax.swing.JComponent -import javax.swing.event.HyperlinkEvent - -class ServerCertificates( - private val project: Project?, - private val context: DeveloperUiToolContext, - private val configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : DeveloperUiTool(parentDisposable), DataProvider { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val log = logger() - - private val url = configuration.register("url", "", INPUT, "https://jetbrains.com") - private val followRedirects = configuration.register("followRedirects", true, CONFIGURATION) - private val allowInsecureConnection = configuration.register("allowInsecureConnection", false, CONFIGURATION) - private val certificatesPanel = BorderLayoutPanel() - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - row { - expandableTextField() - .label(UiToolsBundle.message("server-certificates.url")) - .bindText(url) - .resizableColumn() - .align(Align.FILL) - }.bottomGap(BottomGap.NONE) - row { - checkBox(UiToolsBundle.message("server-certificates.follow-redirects")) - .bindSelected(followRedirects) - }.topGap(TopGap.NONE) - row { - checkBox(UiToolsBundle.message("server-certificates.allow-insecure-connection")) - .bindSelected(allowInsecureConnection) - .gap(RightGap.SMALL) - contextHelp(UiToolsBundle.message("server-certificates.allow-insecure-connection-help")) - }.topGap(TopGap.NONE) - - row { - button(UiToolsBundle.message("server-certificates.fetch-server-certificates")) { - val url = url.get() - fetchCertificates( - project = project, - url = url, - allowInsecureConnection = allowInsecureConnection.get(), - onStarted = { setCertificatesResultUi(createFetchingUi()) }, - onSuccess = { setCertificatesResultUi(createCertificatesUi(it)) }, - onCancel = { setCertificatesResultUi(null) }, - onThrowable = { e -> - log.warn("Failed to retrieve server certificates from: $url", e) - setCertificatesResultUi(createFetchingFailedUi(e)) - } - ) - } - } - - row { - cell(certificatesPanel) - .resizableColumn() - .align(Align.FILL) - }.resizableRow().topGap(TopGap.MEDIUM) - } - - private fun setCertificatesResultUi(component: JComponent?) { - ApplicationManager.getApplication().invokeLater { - certificatesPanel.removeAll() - if (component != null) { - certificatesPanel.addToCenter(component) - } - certificatesPanel.revalidate() - certificatesPanel.repaint() - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun createFetchingUi(): JComponent = panel { - row { - label(UiToolsBundle.message("server-certificates.fetch-server-certificates-in-progress")) - .align(Align.FILL) - .resizableColumn() - } - } - - private fun createFetchingFailedUi(e: Throwable): JComponent = panel { - row { - icon(AllIcons.General.BalloonError).gap(RightGap.SMALL) - label(UiToolsBundle.message(UiToolsBundle.message("server-certificates.fetch-server-certificates-failed", "${e::class.simpleName}: ${e.message}"))) - .align(Align.FILL) - .resizableColumn() - } - } - - private fun createCertificatesUi(httpResponse: HttpResponse): JComponent = panel { - group(UiToolsBundle.message("server-certificates.result"), false) { - row { - cell(HyperlinkLabel(UiToolsBundle.message("server-certificates.response", httpResponse.statusCode, httpResponse.statusMessage)).apply { - addHyperlinkListener(createShowHttpResponseHyperlinkHandler(httpResponse, this)) - }) - } - - if (httpResponse.certificates?.isNotEmpty() == true) { - buildCertificatesExportUi(httpResponse.certificates) - - httpResponse.certificates.forEachIndexed { index, certificate -> - group(UiToolsBundle.message("server-certificates.certificate-title", index + 1), false) { - if (certificate is X509Certificate) { - buildCertificatePropertiesUi(certificate) - buildCertificateValidityUi(certificate) - } - buildCertificatesExportUi(listOf(certificate)) - } - } - } - else { - row { - label("${UiToolsBundle.message("server-certificates.no-result")}") - } - } - } - } - - private fun createShowHttpResponseHyperlinkHandler( - httpResponse: HttpResponse, - parentComponent: JComponent - ): HyperlinkAdapter = object : HyperlinkAdapter() { - override fun hyperlinkActivated(e: HyperlinkEvent) { - val content = panel { - row { - cell( - DeveloperToolEditor( - id = "server-certificates-http-response", - context = context, - configuration = configuration, - project = project, - title = null, - editorMode = DeveloperToolEditor.EditorMode.OUTPUT, - parentDisposable = parentDisposable - ).apply { - text = with(StringJoiner(System.lineSeparator())) { - add("${httpResponse.protocol} ${httpResponse.statusCode} ${httpResponse.statusMessage}") - httpResponse.headers.forEach { - add("${it.key}: ${it.value.joinToString(", ") { it ?: "" }}") - } - httpResponse.body?.let { - add("") - add(it) - } - toString() - } - }.component - ).resizableColumn().align(Align.FILL) - }.resizableRow() - } - createPopup(content).showInCenterOf(parentComponent) - } - } - - private fun Panel.buildCertificatesExportUi(certificates: List) { - row { - lateinit var exportActionsButton: JComponent - exportActionsButton = AnActionOptionButton( - ShowAsPemAction(certificates, context, configuration, project, parentDisposable) { exportActionsButton }, - ExportAsPemAction(certificates, url.get()), - ExportAsDerAction(certificates, url.get()), - ExportAsJksAction(certificates, url.get()), - CopyAsPemToClipboardAction(certificates), - ShowCertificateDetailsAction(certificates, context, configuration, project, parentDisposable) { exportActionsButton } - ) - cell(exportActionsButton) - } - } - - private fun Panel.buildCertificatePropertiesUi(certificate: X509Certificate) { - listOf>( - UiToolsBundle.message("server-certificates.certificate-subject") to certificate.subjectX500Principal, - UiToolsBundle.message("server-certificates.certificate-issuer") to certificate.issuerX500Principal, - UiToolsBundle.message("server-certificates.certificate-serial-number") to certificate.serialNumber, - UiToolsBundle.message("server-certificates.certificate-valid-from") to certificate.notBefore, - UiToolsBundle.message("server-certificates.certificate-valid-to") to certificate.notAfter, - UiToolsBundle.message("server-certificates.certificate-signature-algorithm") to certificate.sigAlgName - ).forEach { (title, value) -> - row("$title:") { - val stringValue = when (value) { - is String -> value - is BigInteger -> value.toString(16).uppercase() - is X500Principal -> value.toString() - is Date -> { - val diff = DateFormatUtil.formatBetweenDates(value.time, System.currentTimeMillis()) - "${DateFormatUtil.formatDateTime(value)} (${diff.capitalize()})" - } - - else -> throw IllegalStateException("Unknown property type: ${value::class}") - } - cell(JBLabel(stringValue).copyable()).gap(RightGap.SMALL) - } - } - } - - private fun Panel.buildCertificateValidityUi(certificate: X509Certificate) { - row { - try { - certificate.checkValidity() - } catch (_: CertificateExpiredException) { - icon(AllIcons.General.Warning).gap(RightGap.SMALL) - label(UiToolsBundle.message("server-certificates.certificate-expired")) - } catch (_: CertificateNotYetValidException) { - icon(AllIcons.General.Warning).gap(RightGap.SMALL) - label(UiToolsBundle.message("server-certificates.certificate-not-valid-yet")) - } - } - } - - private fun fetchCertificates( - project: Project?, - url: String, - allowInsecureConnection: Boolean, - onStarted: () -> Unit, - onSuccess: (HttpResponse) -> Unit, - onCancel: () -> Unit, - onThrowable: (Throwable) -> Unit - ) { - object : Task.Backgroundable(project, UiToolsBundle.message("server-certificates.fetch-server-certificates-in-progress-title"), true) { - override fun run(indicator: ProgressIndicator) { - indicator.text = UiToolsBundle.message("server-certificates.fetch-server-certificates-in-progress") - onStarted() - - val httpClientBuilder = OkHttpClient.Builder() - .followRedirects(followRedirects.get()) - .followSslRedirects(followRedirects.get()) - .applyIntelliJProxySettings(url) - - val certificateCapturingTrustManager = CertificateCapturingTrustManager(allowInsecureConnection) - httpClientBuilder.sslSocketFactory(certificateCapturingTrustManager.createSslContext().socketFactory, certificateCapturingTrustManager) - if (allowInsecureConnection) { - httpClientBuilder.hostnameVerifier(HostnameVerifier { _, _ -> true }) - } - - val httpClient = httpClientBuilder.build() - val request = Request.Builder().url(url).build() - val response = httpClient.newCall(request).execute() - val httpResponse = HttpResponse( - certificates = certificateCapturingTrustManager.serverCertificates, - protocol = OkHttpClientUtils.toDisplayableString(response.protocol), - statusCode = response.code, - statusMessage = OkHttpClientUtils.toStatusMessage(response.code) ?: "", - headers = response.headers.toMultimap(), - body = response.body?.string() - ) - onSuccess(httpResponse) - } - - override fun onCancel() { - onCancel() - } - - override fun onThrowable(error: Throwable) { - onThrowable(error) - } - }.queue() - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private abstract class ExportCertificateAction( - private val formatName: String, - private val certificates: List, - private val url: String - ) : AnAction(UiToolsBundle.message("server-certificates.export-action-title", formatName.uppercase()), null, AllIcons.Actions.MenuSaveall) { - - override fun actionPerformed(e: AnActionEvent) { - try { - val fileSaverDescriptor = FileSaverDescriptor(e.presentation.text, "") - val saveFileDialog: FileSaverDialog = FileChooserFactory.getInstance().createSaveFileDialog(fileSaverDescriptor, e.project) - val defaultFileName = createDefaultCertificateFileName() - saveFileDialog.save(defaultFileName) - ?.file?.toPath() - ?.let { targetPath -> - Files.write(targetPath, createFileContent(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE) - } - onSuccess(e) - } catch (exception: Exception) { - Messages.showErrorDialog( - e.project, - UiToolsBundle.message("server-certificates.export-failed", exception.message ?: ""), - e.presentation.text - ) - } - } - - open fun onSuccess(e: AnActionEvent) { - // Override if needed - } - - abstract fun createFileContent(): ByteArray - - fun X509Certificate.getCn(): String? = - subjectX500Principal?.name?.let { - Regex("CN=(?[^,]+)").find(it)?.groups?.get("cn")?.value - } - - private fun createDefaultCertificateFileName(): String { - val fileName = if (certificates.size > 1) { - try { - "server_certificates_chain_${URI.create(url).host.makeSafeForFilename()}" - } catch (_: Exception) { - "server_certificates_chain" - } - } - else { - certificates[0].safeCastTo() - ?.getCn() - ?.makeSafeForFilename() - ?: "server_certificate" - } - return "$fileName.${formatName.lowercase()}" - } - - private fun String.makeSafeForFilename(): String = - this.replace(Regex("[^a-zA-Z0-9]+"), "_") - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ExportAsPemAction( - private val certificates: List, - url: String - ) : ExportCertificateAction("PEM", certificates, url) { - - override fun createFileContent(): ByteArray = - certificates.flatMap { cert -> - listOf( - "-----BEGIN CERTIFICATE-----", - Base64.getMimeEncoder(64, "\n".toByteArray()).encodeToString(cert.encoded), - "-----END CERTIFICATE-----" - ) - }.joinToString(System.lineSeparator()).toByteArray(StandardCharsets.UTF_8) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ExportAsDerAction( - private val certificates: List, - url: String - ) : ExportCertificateAction("DER", certificates, url) { - - override fun createFileContent(): ByteArray = - certificates.flatMap { it.encoded.asList() }.toByteArray() - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ExportAsJksAction( - private val certificates: List, - url: String - ) : ExportCertificateAction("JKS", certificates, url) { - - private val password = "changeit" - - override fun createFileContent(): ByteArray { - val keyStore = KeyStore.getInstance("JKS") - keyStore.load(null, password.toCharArray()) - - certificates.forEachIndexed { index, certificate -> - val alias = certificate.safeCastTo()?.getCn() ?: "server-certificate-$index" - keyStore.setCertificateEntry(alias, certificate) - } - - val outputStream = ByteArrayOutputStream() - outputStream.use { os -> - keyStore.store(os, password.toCharArray()) - } - return outputStream.toByteArray() - } - - override fun onSuccess(e: AnActionEvent) { - Messages.showInfoMessage( - e.project, - UiToolsBundle.message("server-certificates.export-jks-result", password), - e.presentation.text - ) - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class CopyAsPemToClipboardAction( - private val certificates: List - ) : AnAction(UiToolsBundle.message("server-certificates.copy-pem-to-clipboard-action-title"), null, AllIcons.Actions.Copy) { - - override fun actionPerformed(e: AnActionEvent) { - val pemFile = toPemFile(certificates) - CopyPasteManager.getInstance().setContents(StringSelection(pemFile)) - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ShowAsPemAction( - private val certificates: List, - private val context: DeveloperUiToolContext, - private val configuration: DeveloperToolConfiguration, - private val project: Project?, - private val parentDisposable: Disposable, - private val parentComponent: () -> JComponent - ) : AnAction(UiToolsBundle.message("server-certificates.show-as-pem-action-title"), null, null) { - - override fun actionPerformed(e: AnActionEvent) { - val content = panel { - row { - cell( - DeveloperToolEditor( - id = "server-certificates-show-certificates-as-pem", - context = context, - configuration = configuration, - project = project, - title = null, - editorMode = DeveloperToolEditor.EditorMode.OUTPUT, - parentDisposable = parentDisposable - ).apply { - text = toPemFile(certificates) - }.component - ).resizableColumn().align(Align.FILL) - }.resizableRow() - } - createPopup(content).show(RelativePoint.getSouthOf(parentComponent()), Balloon.Position.below) - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ShowCertificateDetailsAction( - private val certificates: List, - private val context: DeveloperUiToolContext, - private val configuration: DeveloperToolConfiguration, - private val project: Project?, - private val parentDisposable: Disposable, - private val parentComponent: () -> JComponent - ) : AnAction(UiToolsBundle.message("server-certificates.show-certificate-details-action-title"), null, null) { - - override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.EDT - - override fun update(e: AnActionEvent) { - e.presentation.isEnabledAndVisible = certificates.size == 1 - } - - override fun actionPerformed(e: AnActionEvent) { - val content = panel { - row { - cell( - DeveloperToolEditor( - id = "server-certificates-show-details", - context = context, - configuration = configuration, - project = project, - title = null, - editorMode = DeveloperToolEditor.EditorMode.OUTPUT, - parentDisposable = parentDisposable - ).apply { - text = certificates[0].toString() - }.component - ).resizableColumn().align(Align.FILL) - }.resizableRow() - } - createPopup(content).showInCenterOf(parentComponent()) - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private data class HttpResponse( - val certificates: List?, - val protocol: String, - val statusCode: Int, - val statusMessage: String, - val headers: Map>, - val body: String? - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class CertificateCapturingTrustManager( - private val allowInsecureConnection: Boolean - ) : X509TrustManager { - val serverCertificates = mutableListOf() - - override fun checkClientTrusted(chain: Array?, authType: String?) { - // Nothing to do - } - - override fun checkServerTrusted(chain: Array?, authType: String?) { - serverCertificates.clear() - chain?.forEach { serverCertificates.add(it) } - - if (!allowInsecureConnection) { - try { - val defaultTrustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - defaultTrustManager.init(null as KeyStore?) - val defaultX509TrustManager = defaultTrustManager.trustManagers.firstOrNull() as? X509TrustManager - ?: throw IllegalStateException("Default trust manager not available") - defaultX509TrustManager.checkServerTrusted(chain, authType) - } catch (e: Exception) { - throw IllegalStateException("Failed to validate server certificate: ${e.message}", e) - } - } - } - - override fun getAcceptedIssuers(): Array = arrayOf() - - fun createSslContext(): SSLContext = - with(SSLContext.getInstance("TLS")) { - init(null, arrayOf(this@CertificateCapturingTrustManager), SecureRandom()) - this - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = UiToolsBundle.message("server-certificates.menu-title"), - contentTitle = UiToolsBundle.message("server-certificates.content-title") - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> ServerCertificates) = - { configuration -> ServerCertificates(project, context, configuration, parentDisposable) } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - fun toPemFile(certificates: List) = - certificates.flatMap { cert -> - listOf( - "-----BEGIN CERTIFICATE-----", - Base64.getMimeEncoder(64, "\n".toByteArray()).encodeToString(cert.encoded), - "-----END CERTIFICATE-----" - ) - }.joinToString(System.lineSeparator()) - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/TextDiffViewer.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/TextDiffViewer.kt deleted file mode 100644 index 2e656364..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/TextDiffViewer.kt +++ /dev/null @@ -1,113 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other - -import com.intellij.diff.DiffContentFactory -import com.intellij.diff.DiffManagerEx -import com.intellij.diff.contents.DiffContent -import com.intellij.diff.requests.SimpleDiffRequest -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.runWriteAction -import com.intellij.openapi.editor.EditorFactory -import com.intellij.openapi.editor.event.DocumentListener -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.Panel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -class TextDiffViewer( - configuration: DeveloperToolConfiguration, - private val project: Project?, - parentDisposable: Disposable -) : DeveloperUiTool(parentDisposable) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val firstText = configuration.register("firstText", "", INPUT, FIRST_TEXT_EXAMPLE) - private val secondText = configuration.register("secondText", "", INPUT, SECOND_TEXT_EXAMPLE) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - wrapComponentInScrollPane = false - } - - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - row { - val firstDiffContent = createDiffContent(firstText) - val secondDiffContent = createDiffContent(secondText) - val diffComponent = DiffManagerEx.getInstance().createRequestPanel(project, parentDisposable, null).apply { - setRequest(SimpleDiffRequest(null, firstDiffContent, secondDiffContent, null, null)) - }.component - cell(diffComponent).resizableColumn().align(Align.FILL) - }.resizableRow() - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun createDiffContent(textProperty: ValueProperty) : DiffContent { - val document = EditorFactory.getInstance().createDocument(textProperty.get()).apply { - addDocumentListener(object : DocumentListener { - override fun documentChanged(event: com.intellij.openapi.editor.event.DocumentEvent) { - textProperty.set(event.document.text, TEXT_CHANGE_FROM_DOCUMENT_LISTENER) - } - }, parentDisposable) - } - - textProperty.afterChangeConsumeEvent(parentDisposable) { event -> - if (event.id != TEXT_CHANGE_FROM_DOCUMENT_LISTENER) { - runWriteAction { document.setText(event.newValue) } - } - } - - return DiffContentFactory.getInstance().create(project, document) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = - DeveloperUiToolPresentation( - menuTitle = "Text Diff", - contentTitle = "Text Diff Viewer" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> TextDiffViewer) = - { configuration -> - TextDiffViewer( - configuration = configuration, - project = project, - parentDisposable = parentDisposable - ) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val TEXT_CHANGE_FROM_DOCUMENT_LISTENER = "documentChangeListener" - - private val FIRST_TEXT_EXAMPLE = """ - The sky is blue, - The sun is shining, - And the birds are singing. - """.trimIndent() - - private val SECOND_TEXT_EXAMPLE = """ - The sky is gray, - The rain is pouring, - And the birds are silent. - """.trimIndent() - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/Unarchiver.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/Unarchiver.kt deleted file mode 100644 index 84b1aa88..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/other/Unarchiver.kt +++ /dev/null @@ -1,1663 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other - -import com.intellij.icons.AllIcons -import com.intellij.ide.BrowserUtil -import com.intellij.ide.CommonActionsManager -import com.intellij.ide.dnd.FileCopyPasteUtil -import com.intellij.ide.util.treeView.NodeRenderer -import com.intellij.notification.NotificationType -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.ActionUpdateThread -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.DataProvider -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.logger -import com.intellij.openapi.editor.EditorDropHandler -import com.intellij.openapi.fileChooser.FileChooser -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.fileEditor.FileEditorManager -import com.intellij.openapi.fileEditor.OpenFileDescriptor -import com.intellij.openapi.fileEditor.impl.EditorWindow -import com.intellij.openapi.ide.CopyPasteManager -import com.intellij.openapi.progress.ProgressIndicator -import com.intellij.openapi.progress.Task -import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.popup.Balloon -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.util.NlsActions -import com.intellij.openapi.util.text.StringUtil -import com.intellij.openapi.vfs.StandardFileSystems -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.VirtualFileManager -import com.intellij.ui.AnimatedIcon -import com.intellij.ui.DoubleClickListener -import com.intellij.ui.FilteringTree -import com.intellij.ui.HyperlinkLabel -import com.intellij.ui.JBColor -import com.intellij.ui.ScrollPaneFactory -import com.intellij.ui.SimpleTextAttributes -import com.intellij.ui.awt.RelativePoint -import com.intellij.ui.components.JBLabel -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.LabelPosition -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindSelected -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.table.JBTable -import com.intellij.ui.tree.TreeVisitor -import com.intellij.ui.treeStructure.Tree -import com.intellij.util.PlatformIcons -import com.intellij.util.SystemProperties -import com.intellij.util.io.URLUtil -import com.intellij.util.text.DateFormatUtil -import com.intellij.util.ui.UIUtil -import com.intellij.util.ui.components.BorderLayoutPanel -import com.intellij.util.ui.tree.TreeUtil -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.CopyValuesAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.NotificationUtils -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.PluginCommonDataKeys.SELECTED_VALUES -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils.createContextMenuMouseListener -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils.createToggleAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other.Unarchiver.OpenUnarchiverContext -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolHandler -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolReference -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolService -import dev.turingcomplete.intellijdevelopertoolsplugin.common.extension -import dev.turingcomplete.intellijdevelopertoolsplugin.common.nameWithoutExtension -import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo -import dev.turingcomplete.intellijdevelopertoolsplugin.common.uncheckedCastTo -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.ChangeListener -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.ResetListener -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import io.ktor.util.* -import org.apache.commons.compress.archivers.ArchiveEntry -import org.apache.commons.compress.archivers.ArchiveInputStream -import org.apache.commons.compress.archivers.ArchiveStreamFactory -import org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry -import org.apache.commons.compress.archivers.sevenz.SevenZFile -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry -import org.apache.commons.compress.archivers.zip.ZipMethod -import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream -import org.apache.commons.compress.compressors.gzip.GzipUtils -import org.apache.commons.io.FileUtils -import org.apache.commons.io.IOUtils -import java.awt.datatransfer.DataFlavor -import java.awt.datatransfer.StringSelection -import java.awt.datatransfer.Transferable -import java.awt.dnd.DropTarget -import java.awt.dnd.DropTargetDragEvent -import java.awt.dnd.DropTargetDropEvent -import java.awt.dnd.DropTargetEvent -import java.awt.dnd.DropTargetListener -import java.awt.event.MouseEvent -import java.io.BufferedInputStream -import java.io.FileInputStream -import java.io.IOException -import java.io.InputStream -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.nio.file.StandardOpenOption.CREATE_NEW -import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING -import java.nio.file.StandardOpenOption.WRITE -import java.nio.file.attribute.BasicFileAttributeView -import java.nio.file.attribute.BasicFileAttributes -import java.nio.file.attribute.FileTime -import java.util.zip.ZipEntry -import javax.swing.Icon -import javax.swing.JComponent -import javax.swing.JTree -import javax.swing.SwingConstants -import javax.swing.TransferHandler -import javax.swing.event.HyperlinkEvent -import javax.swing.table.DefaultTableModel -import javax.swing.tree.DefaultMutableTreeNode -import javax.swing.tree.DefaultTreeModel -import javax.swing.tree.TreePath - - -internal class Unarchiver( - private val project: Project?, - private val configuration: DeveloperToolConfiguration, - parentDisposable: Disposable -) : DeveloperUiTool(parentDisposable), DataProvider, ChangeListener, ResetListener, OpenDeveloperToolHandler { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val archiveTreeSortingMode = configuration.register("archiveTreeSortingMode", DEFAULT_SORTING_MODE) - private val showArchiveNodeUncompressedSize = configuration.register("showArchiveNodeUncompressedSize", DEFAULT_SHOW_ARCHIVE_NODE_SIZE) - private val showArchiveNodeTotalNumberOfChildren = - configuration.register("showArchiveNodeTotalNumberOfChildren", DEFAULT_SHOW_ARCHIVE_NODE_TOTAL_NUMBER_OF_CHILDREN) - private val openFileInEditorOnDoubleClick = configuration.register("openFileInEditorOnDoubleClick", DEFAULT_OPEN_IN_EDITOR_ON_DOUBLE_CLICK) - private val lastSelectedOpenedDirectoryPath = configuration.register("lastSelectedOpenedDirectoryPath", DEFAULT_LAST_OPENED_DIRECTORY_PATH) - private val lastSelectedTargetDirectoryPath = configuration.register("lastSelectedTargetDirectoryPath", DEFAULT_LAST_SELECTED_TARGET_DIRECTORY_PATH) - private val createArchiveFilenameSubDirectory = configuration.register("createArchiveFilenameSubDirectory", DEFAULT_CREATE_ARCHIVE_FILENAME_SUB_DIRECTORY) - private val clearTargetDirectory = configuration.register("clearTargetDirectory", DEFAULT_CLEAR_TARGET_DIRECTORY) - private val createParentDirectories = configuration.register("createParentDirectories", DEFAULT_CREATE_PARENT_DIRECTORIES) - private val preserveDirectoryStructure = configuration.register("preserveDirectoryStructure", DEFAULT_PRESERVE_DIRECTORY_STRUCTURE) - private val preserveFileAttributes = configuration.register("preserveFileAttributes", DEFAULT_PRESERVE_FILE_ATTRIBUTES) - private val openTargetDirectoryAfterExtraction = configuration.register("openTargetDirectoryAfterExtraction", DEFAULT_OPEN_TARGET_DIRECTORY_AFTER_EXTRACTION) - - private var tree: ArchiveTree? = null - private val content = BorderLayoutPanel() - private val noArchiveFilePanel by lazy { createNoArchiveFilePanel() } - private val readingArchiveFilePanel by lazy { createReadingArchiveFilePanel() } - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun Panel.buildUi() { - content.dropTarget = DropTarget(content, FilesDropHandler(project, openArchiveFile())) - content.addToCenter(noArchiveFilePanel) - - row { - cell(content) - .resizableColumn() - .align(Align.FILL) - }.resizableRow() - } - - override fun afterBuildUi() { - configuration.addChangeListener(parentDisposable, this) - configuration.addResetListener(parentDisposable, this) - } - - override fun configurationReset() { - closeArchiveFile() - } - - override fun configurationChanged(property: ValueProperty) { - tree?.let { - val treeModel = it.tree.model as DefaultTreeModel - - if (property == archiveTreeSortingMode) { - doSortArchiveTree((treeModel.root as DefaultMutableTreeNode).userObject as DirectoryNode) - } - - val expandedPaths = TreeUtil.collectExpandedPaths(it.tree) - treeModel.reload() - it.searchModel.updateStructure() - TreeUtil.restoreExpandedPaths(it.tree, expandedPaths) - } - } - - /** - * This method may get called before the [tree] was initialized, for example, - * during a drop of a file. - */ - override fun getData(dataId: String): Any? = - when { - SELECTED_VALUES.`is`(dataId) -> tree?.getSelectedArchiveNodes() - else -> super.getData(dataId) - } - - override fun applyOpenDeveloperToolContext(context: OpenUnarchiverContext) { - openArchiveFile().invoke(context.archiveFilePath) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun replaceContent(centerContent: JComponent) { - content.removeAll() - content.addToCenter(centerContent) - content.revalidate() - content.repaint() - } - - private fun openArchiveFile(): (Path) -> Unit = { archiveFilePath -> - lastSelectedOpenedDirectoryPath.set(archiveFilePath.parent.toString()) - - val previousExpandedRelativePaths = mutableSetOf() - ApplicationManager.getApplication().invokeAndWait { - tree?.let { tree -> - TreeUtil.collectExpandedPaths(tree.tree) - .mapNotNull { treePath -> ArchiveNode.findInLastPathComponent(treePath) } - .forEach { previousExpandedRelativePaths.add(it.relativePath) } - } - - replaceContent(readingArchiveFilePanel) - } - - ApplicationManager.getApplication().executeOnPooledThread { - try { - val rootNode = buildArchiveTree(archiveFilePath) - doSortArchiveTree(rootNode) - - ApplicationManager.getApplication().invokeLater { - replaceContent(createArchiveTreePanel(archiveFilePath, rootNode)) - - if (previousExpandedRelativePaths.isNotEmpty()) { - TreeUtil.promiseExpand(tree!!.tree) { treePath -> - val archiveNode = ArchiveNode.findInLastPathComponent(treePath) - if (archiveNode != null && previousExpandedRelativePaths.contains(archiveNode.relativePath)) { - TreeVisitor.Action.CONTINUE - } - else { - TreeVisitor.Action.SKIP_CHILDREN - } - } - } - } - } catch (e: Exception) { - log.warn("Reading archive file failed", e) - ApplicationManager.getApplication().invokeLater { - replaceContent(noArchiveFilePanel) - Messages.showErrorDialog(project, "Reading archive failed: ${e.message}", "Reading Archive Failed") - } - } - } - } - - private fun buildArchiveTree(archiveFilePath: Path) = RootNode(archiveFilePath).apply { - val normalizedPathToDirectoryNode = mutableMapOf() - normalizedPathToDirectoryNode[""] = this - iterateEntries { archiveEntry, _ -> - val path = archiveEntry.name - val normalizedPath = path.trimEnd('/') - val parentNode: DirectoryNode = buildParentDirectoryTreeStructure(this, normalizedPath, normalizedPathToDirectoryNode) { - it.totalChildren += 1 - it.addUncompressedSize(archiveEntry.size) - } - val name = normalizedPath.substringAfterLast("/", normalizedPath) - val isDirectory = path.endsWith("/") - if (!isDirectory) { - val fileNode = FileNode(name, archiveEntry) - parentNode.children.add(fileNode) - } - else { - if (!normalizedPathToDirectoryNode.containsKey(normalizedPath)) { - val directoryNode = DirectoryNode(name, path, archiveEntry) - normalizedPathToDirectoryNode[normalizedPath] = directoryNode - parentNode.children.add(directoryNode) - } - else { - normalizedPathToDirectoryNode[normalizedPath]!!.archiveEntry = archiveEntry - } - } - true - } - } - - private fun buildParentDirectoryTreeStructure( - rootNode: DirectoryNode, - normalizedPath: String, - normalizedPathToDirectoryNode: MutableMap, - modifyParentNode: (DirectoryNode) -> Unit - ): DirectoryNode { - val parentPathParts = normalizedPath.split("/") - var currentPath = "" - var parentNode: DirectoryNode = rootNode - for (i in parentPathParts.indices) { - val currentNormalizedPath = currentPath.trimEnd('/') - parentNode = normalizedPathToDirectoryNode.computeIfAbsent(currentNormalizedPath) { - val directoryNode = DirectoryNode(currentNormalizedPath.substringAfterLast("/", currentNormalizedPath), currentPath, null) - parentNode.children.add(directoryNode) - directoryNode - } - modifyParentNode(parentNode) - currentPath += parentPathParts[i] + "/" - } - return parentNode - } - - private fun doSortArchiveTree(node: DirectoryNode) { - node.children.filterIsInstance().forEach { - doSortArchiveTree(it) - } - node.children.sortWith(archiveTreeSortingMode.get().comparator) - } - - private fun createArchiveTreePanel(archiveFilePath: Path, rootNode: RootNode) = panel { - val archiveTree = ArchiveTree(rootNode) - tree = archiveTree - - row { - cell(archiveTree.installSearchField()) - .gap(RightGap.SMALL) - - val actionGroup = DefaultActionGroup().apply { - val commonActionsManager = CommonActionsManager.getInstance() - add(commonActionsManager.createExpandAllHeaderAction(archiveTree.tree)) - add(commonActionsManager.createCollapseAllHeaderAction(archiveTree.tree)) - addSeparator() - add(createSettingsActionGroup()) - add(createReloadAction(archiveFilePath)) - addSeparator() - add(ChooseArchiveFileToOpenAction(project, archiveTree.tree, lastSelectedOpenedDirectoryPath, openArchiveFile())) - } - cell(ActionManager.getInstance().createActionToolbar(Unarchiver::class.qualifiedName!!, actionGroup, true).run { - targetComponent = component - component - }) - }.bottomGap(BottomGap.NONE) - - row { - cell(ScrollPaneFactory.createScrollPane(archiveTree.component, false)) - .align(Align.FILL) - .resizableColumn() - }.resizableRow().topGap(TopGap.NONE) - } - - private fun createSettingsActionGroup() = object : DefaultActionGroup("Settings", true) { - - init { - templatePresentation.icon = AllIcons.General.GearPlain - - add(DefaultActionGroup("Sorting", true).apply { - SortingMode.entries.forEach { - add(createToggleAction(it.title, { archiveTreeSortingMode.get() == it }, { state -> if (state) archiveTreeSortingMode.set(it) })) - } - }) - - add(createToggleAction("Show uncompressed size", { showArchiveNodeUncompressedSize.get() }, { showArchiveNodeUncompressedSize.set(it) })) - add(createToggleAction("Show total number of children", { showArchiveNodeTotalNumberOfChildren.get() }, { showArchiveNodeTotalNumberOfChildren.set(it) })) - - addSeparator() - - add(createToggleAction("Open file in editor on double click", { openFileInEditorOnDoubleClick.get() }, { openFileInEditorOnDoubleClick.set(it) })) - - addSeparator() - - add(createCloseArchiveFileAction()) - } - - @Suppress("UnstableApiUsage") - override fun update(e: AnActionEvent) { - e.presentation.isEnabled = UIUtil.isShowing(tree!!.tree) - } - - override fun getActionUpdateThread() = ActionUpdateThread.EDT - } - - private fun createReloadAction(archiveFilePath: Path): AnAction = - object : DumbAwareAction("Reload", null, AllIcons.Actions.Refresh) { - - override fun actionPerformed(e: AnActionEvent) { - openArchiveFile().invoke(archiveFilePath) - } - - @Suppress("UnstableApiUsage") - override fun update(e: AnActionEvent) { - e.presentation.isEnabled = UIUtil.isShowing(tree!!.tree) - } - - override fun getActionUpdateThread() = ActionUpdateThread.EDT - } - - private fun createCloseArchiveFileAction(): AnAction = object : DumbAwareAction("Close Archive File") { - - override fun actionPerformed(e: AnActionEvent) { - closeArchiveFile() - } - - @Suppress("UnstableApiUsage") - override fun update(e: AnActionEvent) { - e.presentation.isEnabled = UIUtil.isShowing(tree!!.tree) - } - - override fun getActionUpdateThread() = ActionUpdateThread.EDT - } - - private fun closeArchiveFile() { - content.removeAll() - content.addToCenter(noArchiveFilePanel) - content.revalidate() - content.repaint() - } - - private fun createNoArchiveFilePanel(): JComponent { - val actionsPanel = panel { - row { - cell(ChooseArchiveFileToOpenAction(project, null, lastSelectedOpenedDirectoryPath, openArchiveFile()).toHyperlinkLabel()) - } - row { - cell(JBLabel("Drop archive file here to open", AllIcons.Actions.Download, SwingConstants.LEFT)) - }.bottomGap(BottomGap.MEDIUM) - row { - lateinit var supportedTypesLink: JComponent - supportedTypesLink = link("Supported archives and known limitations") { - val text = """ -

    Supported archive types: ${supportedArchiveExtensions.sorted().joinToString(", ")}

    -

    The Apache Commons Compress 1.26.0 library is used for the underlining archive handling,
    which has some known limitations (e.g., encryption is not supported).

    - """.trimIndent() - JBPopupFactory.getInstance() - .createHtmlTextBalloonBuilder(text, null, UIUtil.getPanelBackground()) { e -> - if (e.eventType == HyperlinkEvent.EventType.ACTIVATED) { - BrowserUtil.browse(e.url) - } - } - .setDialogMode(true) - .setBorderColor(JBColor.border()) - .setBlockClicksThroughBalloon(true) - .setRequestFocus(true) - .setCloseButtonEnabled(true) - .createBalloon() - .apply { - setAnimationEnabled(false) - show(RelativePoint.getSouthOf(supportedTypesLink), Balloon.Position.below) - } - }.component - } - } - return panel { - row { - cell(actionsPanel).align(Align.CENTER) - }.resizableRow() - } - } - - private fun createReadingArchiveFilePanel() = panel { - row { - cell(JBLabel("Reading archive...").apply { icon = AnimatedIcon.Default.INSTANCE }) - .align(Align.CENTER) - }.resizableRow() - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private inner class ArchiveTree( - private val rootNode: RootNode - ) : FilteringTree(Tree(), DefaultMutableTreeNode(rootNode)) { - - init { - tree.cellRenderer = ArchiveTreeNodeRenderer( - showArchiveNodeUncompressedSize = showArchiveNodeUncompressedSize, - showArchiveNodeTotalNumberOfChildren = showArchiveNodeTotalNumberOfChildren - ) - - tree.addMouseListener(createContextMenuMouseListener(Unarchiver::class.java.simpleName) { mouseEvent -> - DefaultActionGroup().apply { - add(createExtractAction()) - add(createFactExtractAction()) - - addSeparator() - - add(createOpenInEditorAction()) - add(createOpenWithDefaultApplicationAction()) - add(createOpenEnclosingDirectoryAction()) - add(createCopyContentToClipboardAction()) - - addSeparator() - - add(createShowSelectedElementDetailsAction(mouseEvent)) - - addSeparator() - - add(DefaultActionGroup("Copy", true).apply { - templatePresentation.icon = PlatformIcons.COPY_ICON - add(CopyValuesAction("Filename", { "$it Filenames" }, { (it as ArchiveNode).fileName }, null)) - add(CopyValuesAction("Relative Path", { "$it Relative Paths" }, { (it as ArchiveNode).relativePath()?.toString() }, null)) - add(CopyValuesAction("Absolute Path", { "$it Absolute Paths" }, { (it as ArchiveNode).absolutePath(rootNode.archiveFilePath)?.toString() }, null)) - }) - } - }) - - if (project != null) { - object : DoubleClickListener() { - - override fun onDoubleClick(e: MouseEvent): Boolean { - if (!openFileInEditorOnDoubleClick.get()) { - return false - } - openInEditor(getSelectedArchiveNodes().filterIsInstance(), project, false) - return true - } - }.installOn(tree) - } - } - - private fun createCopyContentToClipboardAction() = - ArchiveNodeAction("Copy Content to Clipboard", { it is FileNode && it.isTextFile() }) { archiveNode, _ -> - ApplicationManager.getApplication().executeOnPooledThread { - try { - val content = rootNode.readEntry(archiveNode.archiveEntry!!) - CopyPasteManager.getInstance().setContents(StringSelection(content.toString(StandardCharsets.UTF_8))) - } catch (e: Exception) { - log.warn("Failed to read archive entry: ${archiveNode.archiveEntry!!.name}", e) - ApplicationManager.getApplication().invokeLater { - Messages.showErrorDialog(project, "Failed to read archive entry: ${e.message}", "Copy Content to Clipboard Failed") - } - } - } - } - - override fun getNodeClass(): Class = DefaultMutableTreeNode::class.java - - override fun createNode(archiveNode: ArchiveNode): DefaultMutableTreeNode = DefaultMutableTreeNode(archiveNode) - - override fun getChildren(archiveNode: ArchiveNode): Iterable = - if (archiveNode is DirectoryNode) archiveNode.children else emptyList() - - override fun getText(archiveNode: ArchiveNode?): String? = archiveNode?.fileName - - private fun createExtractAction() = ArchiveNodesAction("Extract...", { true }) { archiveNodes, _ -> - ApplicationManager.getApplication().executeOnPooledThread { - val archiveNodesToExtract = determineArchiveNodesToExtract(archiveNodes) - if (archiveNodesToExtract.archiveNodes.isEmpty()) { - log.info("No entries to extract found") - return@executeOnPooledThread - } - - ApplicationManager.getApplication().invokeLater { - showExtractionDialog(rootNode, archiveNodesToExtract) { extractionContext -> - ApplicationManager.getApplication().executeOnPooledThread { - ExtractTask(project, extractionContext).queue() - } - } - } - } - } - - private fun createFactExtractAction() = - ArchiveNodesAction("Fast Extract", { true }, "Extracts the selected entry into a temporary directory.") { archiveNodes, _ -> - ApplicationManager.getApplication().executeOnPooledThread { - val archiveNodesToExtract = determineArchiveNodesToExtract(archiveNodes) - if (archiveNodesToExtract.archiveNodes.isEmpty()) { - log.info("No entries to extract found") - return@executeOnPooledThread - } - - val tempDirectory = Files.createTempDirectory("${rootNode.fileName.substringBeforeLast(".")}-") - tempDirectory.toFile().deleteOnExit() - - ExtractTask( - project = project, - extractionContext = ExtractionContext( - rootNode = rootNode, - archiveNodes = archiveNodesToExtract.archiveNodes, - targetDirectoryPath = tempDirectory, - clearTargetDirectory = true, - preserveDirectoryStructure = true, - preserveFileAttributes = true, - createParentDirectories = false, - openTargetDirectoryAfterExtraction = true - ) - ).queue() - } - } - - private fun createOpenEnclosingDirectoryAction() = - ArchiveNodeAction("Open Enclosing Directory...", { it is RootNode }) { rootNode, _ -> - BrowserUtil.browse((rootNode as RootNode).archiveFilePath.parent) - } - - private fun createOpenInEditorAction() = - ArchiveNodesAction("Open in Editor...", { it is FileNode }) { archiveNodes, e -> - val project = e.dataContext.getData(CommonDataKeys.PROJECT) ?: throw IllegalStateException("snh: Data missing") - openInEditor(archiveNodes.map { it as FileNode }, project, true) - } - - private fun openInEditor(archiveNodes: List, project: Project, notifyOnError: Boolean) { - ApplicationManager.getApplication().executeOnPooledThread { - val fileEditorManager = FileEditorManager.getInstance(project) - val virtualFileManager = VirtualFileManager.getInstance() - val virtualFiles = archiveNodes.mapNotNull { virtualFileManager.findFileByUrl("jar://${it.absolutePath(rootNode.archiveFilePath)}") } - ApplicationManager.getApplication().invokeLater { - virtualFiles.forEach { - val openEditor = fileEditorManager.openEditor(OpenFileDescriptor(project, it), true) - if (openEditor.isEmpty() && notifyOnError) { - Messages.showErrorDialog(project, "Unable to open file '${it.name}' in editor.", "Open in Editor Failed") - } - } - } - } - } - - private fun createOpenWithDefaultApplicationAction() = - ArchiveNodeAction("Open With Default Application...", { it is RootNode }) { rootNode, _ -> - BrowserUtil.browse((rootNode as RootNode).archiveFilePath) - } - - private fun createShowSelectedElementDetailsAction(contextMenuMouseEvent: MouseEvent) = - object : DumbAwareAction("Show Details...") { - - override fun update(e: AnActionEvent) { - val selectedValues = SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") - - e.presentation.isVisible = selectedValues.singleOrNull()?.let { - it is RootNode || (it is ArchiveNode && it.archiveEntry != null) - } == true - } - - override fun actionPerformed(e: AnActionEvent) { - val selectedValues = SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") - val archiveNode = selectedValues.singleOrNull()?.uncheckedCastTo(ArchiveNode::class) - if (archiveNode !is RootNode && archiveNode?.archiveEntry == null) { - return - } - - val (title, panel) = when (archiveNode) { - is RootNode -> archiveNode.archiveFilePath.fileName.toString() to createArchiveFileDetails(archiveNode) - else -> "${archiveNode.fileName} (${if (archiveNode is DirectoryNode) "directory" else "file"})" to createArchiveEntryDetails(archiveNode) - } - JBPopupFactory.getInstance().createBalloonBuilder(panel) - .setTitle(title) - .setDialogMode(true) - .setFillColor(UIUtil.getPanelBackground()) - .setBorderColor(JBColor.border()) - .setBlockClicksThroughBalloon(true) - .setRequestFocus(true) - .setCloseButtonEnabled(true) - .createBalloon() - .apply { - setAnimationEnabled(false) - show(RelativePoint(contextMenuMouseEvent.locationOnScreen), Balloon.Position.below) - } - } - - private fun createArchiveEntryDetails(archiveNode: ArchiveNode) = panel { - val archiveEntry = archiveNode.archiveEntry!! - - if (archiveNode is DirectoryNode) { - row { - label("Number of direct entries:") - label(archiveNode.children.size.toString()) - }.layout(RowLayout.PARENT_GRID) - row { - label("Number of all entries:") - label(archiveNode.totalChildren.toString()) - }.layout(RowLayout.PARENT_GRID) - } - - row { - label("Uncompressed size${if (archiveNode is DirectoryNode) " of all entries" else ""}:") - label(StringUtil.formatFileSize(archiveNode.totalUncompressedSize() ?: 0)) - }.layout(RowLayout.PARENT_GRID).bottomGap(BottomGap.NONE) - if (archiveNode.inaccurateTotalUncompressedSize) { - row { - label("The uncompressed size may be inaccurate because some entries do not provide size information.") - }.topGap(TopGap.NONE) - } - - if (archiveEntry is ZipArchiveEntry) { - if (archiveNode is FileNode) { - row { - label("Compressed size:") - label(StringUtil.formatFileSize(archiveEntry.compressedSize)) - }.layout(RowLayout.PARENT_GRID) - } - - row { - label("Method:") - val method = ZipMethod.getMethodByCode(archiveEntry.method) - label(method.name.split("_").joinToString(" ")) - }.layout(RowLayout.PARENT_GRID) - } - - val fileTimes = FileTimes.fromArchiveEntry(archiveEntry) - row { - label("Creation time:") - label(if (fileTimes.creationTime != null) DateFormatUtil.formatDateTime(fileTimes.creationTime.toMillis()) else "Unknown") - }.layout(RowLayout.PARENT_GRID) - row { - label("Last modified time:") - label(if (fileTimes.lastModifiedTime != null) DateFormatUtil.formatDateTime(fileTimes.lastModifiedTime.toMillis()) else "Unknown") - }.layout(RowLayout.PARENT_GRID) - row { - label("Last access time:") - label(if (fileTimes.lastAccessTime != null) DateFormatUtil.formatDateTime(fileTimes.lastAccessTime.toMillis()) else "Unknown") - }.layout(RowLayout.PARENT_GRID) - - if (archiveEntry is ZipEntry) { - row { - label("Comment:") - label(archiveEntry.comment) - }.layout(RowLayout.PARENT_GRID) - } - - if (archiveEntry is ZipEntry && archiveEntry.extra != null) { - row { - label("Extra data size:") - label(StringUtil.formatFileSize(archiveEntry.extra.size.toLong())) - }.layout(RowLayout.PARENT_GRID) - } - } - - override fun getActionUpdateThread() = ActionUpdateThread.EDT - } - - private fun createArchiveFileDetails(rootNode: RootNode) = try { - panel { - row { - label("Number of direct entries:") - label(rootNode.children.size.toString()) - }.layout(RowLayout.PARENT_GRID) - row { - label("Number of all entries:") - label(rootNode.totalChildren.toString()) - }.layout(RowLayout.PARENT_GRID) - val zipFileAttributes = Files.readAttributes(rootNode.archiveFilePath, BasicFileAttributes::class.java) - row { - label("Actual size on disk:") - label(FileUtils.byteCountToDisplaySize(zipFileAttributes.size())) - }.layout(RowLayout.PARENT_GRID) - row { - label("Uncompressed size of all entries:") - label(StringUtil.formatFileSize(rootNode.totalUncompressedSize() ?: 0)) - }.layout(RowLayout.PARENT_GRID) - row { - label("Creation time:") - label(DateFormatUtil.formatDateTime(zipFileAttributes.creationTime().toMillis())) - }.layout(RowLayout.PARENT_GRID) - row { - label("Last modified time:") - label(DateFormatUtil.formatDateTime(zipFileAttributes.lastModifiedTime().toMillis())) - }.layout(RowLayout.PARENT_GRID) - row { - label("Last access time:") - label(DateFormatUtil.formatDateTime(zipFileAttributes.lastAccessTime().toMillis())) - }.layout(RowLayout.PARENT_GRID) - row { - label("Owner:") - label(Files.getOwner(rootNode.archiveFilePath).name) - }.layout(RowLayout.PARENT_GRID) - row { - val writable = Files.isWritable(rootNode.archiveFilePath) - icon(if (writable) AllIcons.Ide.Readwrite else AllIcons.Ide.Readonly).gap(RightGap.SMALL) - label(if (writable) "File is writeable" else "File is readonly") - } - } - } catch (e: IOException) { - log.warn("Failed to read parameters of file: ${rootNode.archiveFilePath}", e) - panel { - row { - label("Failed to read file attributes: ${e.message}").bold() - } - } - } - - fun getSelectedArchiveNodes(): List = - TreeUtil.collectSelectedPaths(tree) - .mapNotNull { - it.lastPathComponent?.uncheckedCastTo(DefaultMutableTreeNode::class) - ?.userObject?.uncheckedCastTo(ArchiveNode::class) - } - .toList() - - private fun showExtractionDialog( - rootNode: RootNode, - archiveNodesToExtract: ArchiveNodesToExtract, - okCallback: (ExtractionContext) -> Unit - ) { - object : DialogWrapper(project, tree, false, IdeModalityType.IDE) { - - init { - title = "Extract" - setSize(600, 400) - isModal = true - setOKButtonText("Extract") - init() - } - - override fun doOKAction() { - okCallback( - ExtractionContext( - rootNode = rootNode, - archiveNodes = archiveNodesToExtract.archiveNodes, - targetDirectoryPath = if (createArchiveFilenameSubDirectory.get()) { - Paths.get(lastSelectedTargetDirectoryPath.get()).resolve(rootNode.archiveFilePath.nameWithoutExtension()) - } - else { - Paths.get(lastSelectedTargetDirectoryPath.get()) - }, - clearTargetDirectory = clearTargetDirectory.get(), - preserveDirectoryStructure = preserveDirectoryStructure.get(), - preserveFileAttributes = preserveFileAttributes.get(), - createParentDirectories = createParentDirectories.get(), - openTargetDirectoryAfterExtraction = openTargetDirectoryAfterExtraction.get() - ) - ) - super.doOKAction() - } - - override fun createCenterPanel(): JComponent = panel { - row { - textFieldWithBrowseButton(FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle("Select Target Directory"), project) - .label("Target directory:") - .bindText(lastSelectedTargetDirectoryPath) - .resizableColumn() - .align(Align.FILL) - } - row { - checkBox("Create a sub-directory with the archive name '${rootNode.archiveFilePath.nameWithoutExtension()}'") - .bindSelected(createArchiveFilenameSubDirectory) - } - row { - checkBox("Clear target directory") - .bindSelected(clearTargetDirectory) - }.bottomGap(BottomGap.SMALL) - - row { - checkBox("Preserve directory structure") - .bindSelected(preserveDirectoryStructure) - .gap(RightGap.SMALL) - contextHelp("If unselected, all files will be extracted as a flat list, ignoring the directory structure.") - } - row { - checkBox("Create parent directories") - .bindSelected(createParentDirectories) - .enabledIf(preserveDirectoryStructure) - .gap(RightGap.SMALL) - contextHelp("For example, if selected, for the path first/second/third/ both parent directories first/second/ will be created, otherwise only the directory third/.") - } - row { - checkBox("Preserve file attributes") - .bindSelected(preserveFileAttributes) - }.bottomGap(BottomGap.SMALL) - - row { - checkBox("Open target directory after extraction") - .bindSelected(openTargetDirectoryAfterExtraction) - }.bottomGap(BottomGap.SMALL) - - row { - val entriesToExtractTable = JBTable( - DefaultTableModel( - archiveNodesToExtract.displayPathsWithUncompressedSize - .map { (displayPath, totalUncompressedSize) -> - arrayOf(displayPath, if (totalUncompressedSize != null) StringUtil.formatFileSize(totalUncompressedSize) else "Unknown") - }.toTypedArray(), - arrayOf("Path", "Uncompressed Size") - ) - ) - cell(ScrollPaneFactory.createScrollPane(entriesToExtractTable)).resizableColumn().align(Align.FILL) - .label("Entries to extract:", LabelPosition.TOP) - }.resizableRow().bottomGap(BottomGap.NONE) - row { - comment( - "Expected size on disk: ${if (archiveNodesToExtract.inaccurateTotalUncompressedSize) "+" else ""}${ - StringUtil.formatFileSize( - archiveNodesToExtract.totalUncompressedSize - ) - }" - ) - }.topGap(TopGap.SMALL) - } - }.show() - } - - private fun determineArchiveNodesToExtract(archiveNodes: List): ArchiveNodesToExtract { - val rootNode = archiveNodes.find { it is RootNode } - return if (rootNode != null) { - ArchiveNodesToExtract( - archiveNodes = listOf(rootNode), - displayPathsWithUncompressedSize = (rootNode as RootNode).children.associate { it.relativePath to it.totalUncompressedSize() }, - totalUncompressedSize = rootNode.totalUncompressedSize() ?: 0, - inaccurateTotalUncompressedSize = rootNode.inaccurateTotalUncompressedSize - ) - } - else { - val finalArchiveNodes = mutableListOf() - val displayPathsWithUncompressedSize = mutableMapOf() - val relativePathToArchiveNode = archiveNodes.associateBy { it.relativePath }.toSortedMap() - var previousRelativePath: String? = null - var totalUncompressedSize = 0L - var inaccurateTotalUncompressedSize = false - for ((relativePath, archiveNode) in relativePathToArchiveNode) { - if (previousRelativePath == null || !relativePath.startsWith(previousRelativePath)) { - finalArchiveNodes.add(archiveNode) - val totalUncompressedSize0 = archiveNode.totalUncompressedSize() - displayPathsWithUncompressedSize[archiveNode.relativePath] = totalUncompressedSize0 - if (totalUncompressedSize0 != null) { - totalUncompressedSize += totalUncompressedSize0 - } - else { - inaccurateTotalUncompressedSize = true - } - previousRelativePath = relativePath - } - } - ArchiveNodesToExtract( - archiveNodes = finalArchiveNodes, - displayPathsWithUncompressedSize = displayPathsWithUncompressedSize, - totalUncompressedSize = totalUncompressedSize, - inaccurateTotalUncompressedSize = inaccurateTotalUncompressedSize - ) - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private data class FileTimes(val lastModifiedTime: FileTime?, val lastAccessTime: FileTime?, val creationTime: FileTime?) { - - companion object { - - fun fromArchiveEntry(archiveEntry: ArchiveEntry): FileTimes { - val (lastModifiedTime, lastAccessTime, creationTime) = when (archiveEntry) { - is ZipEntry -> Triple(archiveEntry.lastModifiedTime, archiveEntry.lastAccessTime, archiveEntry.creationTime) - is SevenZArchiveEntry -> Triple( - if (archiveEntry.hasLastModifiedDate) archiveEntry.lastModifiedTime else null, - if (archiveEntry.hasAccessDate) archiveEntry.accessTime else null, - if (archiveEntry.hasCreationDate) archiveEntry.creationTime else null - ) - - else -> Triple(archiveEntry.lastModifiedDate?.toInstant()?.let { FileTime.from(it) }, null, null) - } - return FileTimes(lastModifiedTime, lastAccessTime, creationTime) - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ArchiveNodesAction( - @NlsActions.ActionText title: String, - private val filter: (Any) -> Boolean, - @NlsActions.ActionDescription description: String? = null, - private val actionPerformed: (List, AnActionEvent) -> Unit - ) : DumbAwareAction(title, description, null) { - - override fun update(e: AnActionEvent) { - val selectedValues = SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") - - e.presentation.isVisible = selectedValues.any(filter) - } - - override fun actionPerformed(e: AnActionEvent) { - val selectedValues = SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") - val archiveNodes = selectedValues.filter(filter).map { it as ArchiveNode }.toList() - actionPerformed(archiveNodes, e) - } - - override fun getActionUpdateThread() = ActionUpdateThread.EDT - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ArchiveNodeAction( - @NlsActions.ActionText title: String, - private val filter: (ArchiveNode) -> Boolean, - private val actionPerformed: (ArchiveNode, AnActionEvent) -> Unit - ) : DumbAwareAction(title) { - - override fun update(e: AnActionEvent) { - val archiveNode = getSelectedArchiveNode(e) - e.presentation.isVisible = archiveNode != null - } - - override fun actionPerformed(e: AnActionEvent) { - val archiveNode = getSelectedArchiveNode(e) ?: return - actionPerformed(archiveNode, e) - } - - private fun getSelectedArchiveNode(e: AnActionEvent): ArchiveNode? { - val selectedValues = SELECTED_VALUES.getData(e.dataContext) ?: throw IllegalStateException("snh: Data missing") - return selectedValues.singleOrNull()?.uncheckedCastTo(ArchiveNode::class)?.takeIf(filter) - } - - override fun getActionUpdateThread() = ActionUpdateThread.EDT - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private abstract class ArchiveNode( - val fileName: String, - val icon: Icon, - private var totalUncompressedSize: Long = 0, - /** - * Must be a non-normalized path to distinguish between a directory and a file - * with the same name. - */ - val relativePath: String, - var archiveEntry: ArchiveEntry? - ) { - - var inaccurateTotalUncompressedSize = false - private set - - fun addUncompressedSize(size: Long) { - if (size != ArchiveEntry.SIZE_UNKNOWN) { - totalUncompressedSize += size - } - else { - inaccurateTotalUncompressedSize = true - } - } - - fun totalUncompressedSize(): Long? = - if (totalUncompressedSize != ArchiveEntry.SIZE_UNKNOWN) totalUncompressedSize else null - - override fun toString(): String = fileName - - open fun relativePath(): Path? = archiveEntry?.name?.let { Paths.get(it) } - - open fun absolutePath(archiveFilePath: Path): Path? { - if (archiveEntry == null) { - return null - } - - return archiveFilePath.parent.resolve("${archiveFilePath.fileName}!").resolve(Paths.get(archiveEntry!!.name)) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ArchiveNode) return false - - if (relativePath != other.relativePath) return false - - return true - } - - override fun hashCode(): Int = relativePath.hashCode() - - companion object { - - fun toRelativePath(path: String) = - if (path.startsWith("/")) path.substringAfter("/") else path - - fun findInLastPathComponent(treePath: TreePath): ArchiveNode? = treePath.lastPathComponent - ?.safeCastTo() - ?.userObject - ?.safeCastTo() - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class FileNode( - fileName: String, - archiveEntry: ArchiveEntry - ) : ArchiveNode(fileName, determineIcon(fileName), archiveEntry.size, toRelativePath(archiveEntry.name), archiveEntry) { - - override fun relativePath(): Path = super.relativePath()!! - - override fun absolutePath(archiveFilePath: Path): Path = super.absolutePath(archiveFilePath)!! - - fun isTextFile() = textFileNames.contains(fileName) - || textFileExtensions.contains(fileName.substringAfterLast(".").toLowerCasePreservingASCIIRules()) - - companion object { - - private fun determineIcon(fileName: String): Icon { - when (fileName) { - ".htaccess" -> return AllIcons.FileTypes.Htaccess - "config", "init", ".gitignore", ".gitattributes", ".editorconfig", "Dockerfile", "index", "Makefile" -> return AllIcons.FileTypes.Config - "README", "LICENSE", "NOTICE", "CHANGELOG" -> AllIcons.FileTypes.Text - } - - return when (fileName.substringAfterLast(".").toLowerCasePreservingASCIIRules()) { - "mf" -> AllIcons.FileTypes.Manifest - "zip", "tar", "jar" -> AllIcons.FileTypes.Archive - "as" -> AllIcons.FileTypes.AS - "aj" -> AllIcons.FileTypes.Aspectj - "config", "conf", "cfg", "ini" -> AllIcons.FileTypes.Config - "css", "scss", "sass" -> AllIcons.FileTypes.Css - "dtd" -> AllIcons.FileTypes.Dtd - "hprof" -> AllIcons.FileTypes.Hprof - "html", "htm" -> AllIcons.FileTypes.Html - "xhtml" -> AllIcons.FileTypes.Xhtml - "xml", "pom" -> AllIcons.FileTypes.Xml - "xsd" -> AllIcons.FileTypes.XsdFile - "jpg", "jpeg", "png", "gif", "bmp", "tiff", "svg", "psd", "ai", "eps", "raw", "webp", "pdf", "ico" -> AllIcons.FileTypes.Image - "java", "kt", "kts", "groovy", "scala" -> AllIcons.FileTypes.Java - "class", "jmod" -> AllIcons.FileTypes.JavaClass - "js" -> AllIcons.FileTypes.JavaScript - "jfr" -> AllIcons.FileTypes.Jfr - "json" -> AllIcons.FileTypes.Json - "yaml", "yml" -> AllIcons.FileTypes.Yaml - "jsp" -> AllIcons.FileTypes.Jsp - "jspx" -> AllIcons.FileTypes.Jspx - "properties" -> AllIcons.FileTypes.Properties - "txt", "text", "md", "log", "sql", "csv", "tex" -> AllIcons.FileTypes.Text - else -> AllIcons.FileTypes.Any_type - } - } - } - - private val textFileNames = setOf( - "config", "init", ".gitignore", ".gitattributes", ".editorconfig", - "Dockerfile", "index", "Makefile", "README", "LICENSE", "NOTICE", - "CHANGELOG", ".htaccess" - ) - - private val textFileExtensions = setOf( - "txt", "html", "xml", "csv", "json", "log", "md", "css", "js", "php", - "py", "java", "c", "cpp", "rb", "sql", "yml", "yaml", "ini", "conf", - "cfg", "xml", "htm", "cs", "scala", "groovy", "sh", "bat", "ps1", "awk", - "sed", "tex", "r", "scss", "less", "styl", "asp", "jsp", "aspx", "xhtml", - "cfc", "cfm", "tpl", "twig", "handlebars", "mustache", "jsx", "tsx", - "kt", "kts", "mf", "config", "properties", "pom", "text", "sass" - ) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private open class DirectoryNode( - name: String, - relativePath: String, - archiveEntry: ArchiveEntry?, - icon: Icon = AllIcons.Nodes.Folder - ) : ArchiveNode(name, icon, 0, toRelativePath(relativePath), archiveEntry) { - - var totalChildren: Int = 0 - val children: MutableList = mutableListOf() - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class RootNode( - val archiveFilePath: Path - ) : DirectoryNode(archiveFilePath.fileName.toString(), "", null, AllIcons.FileTypes.Archive) { - - override fun relativePath(): Path? = null - - override fun absolutePath(archiveFilePath: Path): Path = archiveFilePath - - fun iterateEntries(visitor: (ArchiveEntry, () -> InputStream) -> Boolean) { - if (archiveFilePath.fileName.extension() == "7z") { - val sevenZFile = SevenZFile.Builder().setFile(archiveFilePath.toFile()).get() - var archiveEntry = sevenZFile.nextEntry - while (archiveEntry != null) { - if (!visitor(archiveEntry) { sevenZFile.getInputStream(archiveEntry) }) { - break - } - archiveEntry = sevenZFile.nextEntry - } - } - else { - var archiveFileInputStream = BufferedInputStream(FileInputStream(archiveFilePath.toFile())) - if (GzipUtils.isCompressedFileName(archiveFilePath.fileName.toString())) { - archiveFileInputStream = BufferedInputStream(GzipCompressorInputStream(archiveFileInputStream)) - } - - val archiveInputStream: ArchiveInputStream<*> = ArchiveStreamFactory().createArchiveInputStream(archiveFileInputStream) - archiveInputStream.use { archiveInputStream0 -> - var archiveEntry = archiveInputStream0.nextEntry - while (archiveEntry != null) { - if (!visitor(archiveEntry) { archiveInputStream0 }) { - break - } - archiveEntry = archiveInputStream0.nextEntry - } - } - } - } - - fun readEntry(archiveEntry: ArchiveEntry): ByteArray { - var result: ByteArray? = null - iterateEntries { archiveEntry0, archiveInputStream -> - if (archiveEntry0 == archiveEntry) { - result = IOUtils.toByteArray(archiveInputStream()) - false - } - else { - true - } - } - return result ?: throw IllegalStateException("Unable to find archive entry: ${archiveEntry.name}") - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ChooseArchiveFileToOpenAction( - private val project: Project?, - private val tree: Tree?, - private val lastSelectedOpenedDirectoryPath: ValueProperty, - private val openArchiveCallback: (Path) -> Unit - ) : DumbAwareAction("Open Archive File", null, AllIcons.Actions.MenuOpen) { - - override fun actionPerformed(e: AnActionEvent) { - openArchiveDialog() - } - - @Suppress("UnstableApiUsage") - override fun update(e: AnActionEvent) { - e.presentation.isEnabled = tree != null && UIUtil.isShowing(tree) - } - - override fun getActionUpdateThread() = ActionUpdateThread.EDT - - private fun openArchiveDialog() { - ApplicationManager.getApplication().executeOnPooledThread { - val startPath = VirtualFileManager.getInstance().findFileByUrl(lastSelectedOpenedDirectoryPath.get()) - - ApplicationManager.getApplication().invokeLater { - val descriptor = FileChooserDescriptorFactory.createSingleFileDescriptor() - .withTitle("Open Archive File") - .withExtensionFilter("Archive files", *supportedArchiveExtensions) - val fileToOpen = FileChooser.chooseFile(descriptor, project, startPath) - if (fileToOpen != null) { - openArchiveCallback(fileToOpen.toNioPath()) - } - } - } - } - - @Suppress("DialogTitleCapitalization") - fun toHyperlinkLabel() = HyperlinkLabel(templatePresentation.text).apply { - icon = templatePresentation.icon - - addHyperlinkListener { - openArchiveDialog() - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class OpenArchiveFileInUnarchiverAction : DumbAwareAction( - "Unarchiver", - "Open archive file in the developer tool 'Unarchiver'.", - null - ) { - - private val supportedArchiveExtensions = Companion.supportedArchiveExtensions.toSet() - - override fun update(e: AnActionEvent) { - val selectedFiles = CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(e.dataContext) - e.presentation.isVisible = isSelectedFileSupported(selectedFiles) - } - - override fun actionPerformed(e: AnActionEvent) { - val selectedFiles = CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(e.dataContext) - if (!isSelectedFileSupported(selectedFiles)) { - return - } - - var sanitizedFilePath = selectedFiles!![0].path - if (selectedFiles[0].fileSystem.protocol == StandardFileSystems.JAR_PROTOCOL - && sanitizedFilePath.endsWith(URLUtil.JAR_SEPARATOR) - ) { - sanitizedFilePath = sanitizedFilePath.substringBeforeLast(URLUtil.JAR_SEPARATOR) - } - - e.project?.service() - ?.openTool(OpenUnarchiverContext(Paths.get(sanitizedFilePath)), openUnarchiverReference) - } - - override fun getActionUpdateThread() = ActionUpdateThread.BGT - - private fun isSelectedFileSupported(selectedFiles: Array?) = - selectedFiles?.size == 1 && - selectedFiles.any { it.extension != null && supportedArchiveExtensions.contains(it.extension) } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - internal class FilesDropHandler( - private val project: Project?, - private val openArchive: (Path) -> Unit - ) : TransferHandler(), EditorDropHandler, DropTargetListener { - - override fun canImport(comp: JComponent?, transferFlavors: Array?): Boolean { - return canHandleDrop0(transferFlavors) - } - - override fun canHandleDrop(transferFlavors: Array): Boolean { - return canHandleDrop0(transferFlavors) - } - - override fun drop(event: DropTargetDropEvent) { - event.acceptDrop(event.dropAction) - handleDrop0(event.transferable) - } - - override fun handleDrop(transferable: Transferable, project: Project?, editorWindow: EditorWindow?) { - handleDrop0(transferable) - } - - override fun importData(comp: JComponent?, transferable: Transferable): Boolean { - return handleDrop0(transferable) - } - - override fun dragEnter(dtde: DropTargetDragEvent?) { - // Nothing to do - } - - override fun dragOver(dtde: DropTargetDragEvent?) { - // Nothing to do - } - - override fun dropActionChanged(dtde: DropTargetDragEvent?) { - // Nothing to do - } - - override fun dragExit(dte: DropTargetEvent?) { - // Nothing to do - } - - private fun canHandleDrop0(transferFlavors: Array?): Boolean { - return transferFlavors != null && FileCopyPasteUtil.isFileListFlavorAvailable(transferFlavors) - } - - private fun handleDrop0(transferable: Transferable): Boolean { - try { - FileCopyPasteUtil.getFiles(transferable) - ?.takeIf { it.size == 1 } - ?.let { - openArchive(it[0]) - return true - } - } catch (e: Exception) { - Messages.showErrorDialog(project, "Failed to handle dropped file: ${e.message}.", "Open Archive") - } - return false - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class SortingMode( - val title: String, - val comparator: Comparator - ) { - - UNCOMPRESSED_SIZE_ASC("Uncompressed size (ascending)", { a, b -> compareValues(a.totalUncompressedSize(), b.totalUncompressedSize()) }), - UNCOMPRESSED_SIZE_DESC("Uncompressed size (descending)", { a, b -> compareValues(b.totalUncompressedSize(), a.totalUncompressedSize()) }), - FILENAME_ASC("Filename (ascending)", { a, b -> a.fileName.compareTo(b.fileName) }), - FILENAME_DESC("Filename (descending)", { a, b -> b.fileName.compareTo(a.fileName) }) - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ArchiveTreeNodeRenderer( - val showArchiveNodeUncompressedSize: ValueProperty, - val showArchiveNodeTotalNumberOfChildren: ValueProperty - ) : NodeRenderer() { - - override fun customizeCellRenderer(tree: JTree, - value: Any, - selected: Boolean, - expanded: Boolean, - leaf: Boolean, - row: Int, - hasFocus: Boolean) { - val archiveNode = value.uncheckedCastTo(DefaultMutableTreeNode::class).userObject.uncheckedCastTo(ArchiveNode::class) - - icon = archiveNode.icon - append(archiveNode.fileName, SimpleTextAttributes.REGULAR_ATTRIBUTES) - - val metaInformation = mutableListOf() - if (showArchiveNodeUncompressedSize.get()) { - archiveNode.totalUncompressedSize()?.let { - if (!(it == 0L && archiveNode.inaccurateTotalUncompressedSize)) { - metaInformation.add("${if (archiveNode.inaccurateTotalUncompressedSize) "~" else ""}${StringUtil.formatFileSize(it)}") - } - } - } - if (showArchiveNodeTotalNumberOfChildren.get() && archiveNode is DirectoryNode) { - metaInformation.add("${archiveNode.totalChildren} entries") - } - if (metaInformation.isNotEmpty()) { - append(" ", SimpleTextAttributes.REGULAR_ATTRIBUTES) - append(metaInformation.joinToString(", "), SimpleTextAttributes.GRAY_SMALL_ATTRIBUTES) - } - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private data class ExtractionContext( - val rootNode: RootNode, - val archiveNodes: List, - val targetDirectoryPath: Path, - val clearTargetDirectory: Boolean, - val createParentDirectories: Boolean, - val preserveDirectoryStructure: Boolean, - val preserveFileAttributes: Boolean, - val openTargetDirectoryAfterExtraction: Boolean - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private data class ArchiveNodesToExtract( - val archiveNodes: List, - val displayPathsWithUncompressedSize: Map, - val totalUncompressedSize: Long, - val inaccurateTotalUncompressedSize: Boolean - ) - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ExtractTask( - project: Project?, - private val extractionContext: ExtractionContext - ) : Task.ConditionalModal( - project, - "Extracting ${extractionContext.rootNode.fileName}", - true, - DEAF - ) { - - override fun run(progressIndicator: ProgressIndicator) { - progressIndicator.checkCanceled() - progressIndicator.text = "Preparing directory structure..." - - if (!Files.exists(extractionContext.targetDirectoryPath)) { - Files.createDirectories(extractionContext.targetDirectoryPath) - } - else { - if (!Files.isDirectory(extractionContext.targetDirectoryPath)) { - throw IllegalArgumentException("The target path '${extractionContext.targetDirectoryPath}' already exists but it is not a directory") - } - else if (extractionContext.clearTargetDirectory) { - FileUtils.cleanDirectory(extractionContext.targetDirectoryPath.toFile()) - } - } - - // Prepare directories... - val preparedFileNodeExtractions = mutableMapOf() - extractionContext.archiveNodes.forEach { archiveNode -> - progressIndicator.checkCanceled() - progressIndicator.text2 = archiveNode.relativePath - - when (archiveNode) { - is DirectoryNode -> preparedFileNodeExtractions.putAll( - prepareDirectoryNode(archiveNode, extractionContext.targetDirectoryPath, extractionContext) - ) - - is FileNode -> prepareStandaloneFileNodeExtraction(archiveNode, extractionContext).let { (archiveEntry, path) -> - preparedFileNodeExtractions[archiveEntry.name] = path - } - - else -> throw IllegalStateException("Unknown archive node type: ${archiveNode::class.qualifiedName}") - } - } - - // Copy files... - val numOfFileNodesToExtract = preparedFileNodeExtractions.size - val archiveEntryToTargetFilePathToCopy = preparedFileNodeExtractions.toMutableMap() - extractionContext.rootNode.iterateEntries { archiveEntry, archiveInputStream -> - if (archiveEntryToTargetFilePathToCopy.containsKey(archiveEntry.name)) { - progressIndicator.checkCanceled() - progressIndicator.text = - "Extracting $numOfFileNodesToExtract file${if (numOfFileNodesToExtract == 1) "s" else ""} (${archiveEntryToTargetFilePathToCopy.size} remaining)..." - progressIndicator.text2 = archiveEntry.name - progressIndicator.fraction = (numOfFileNodesToExtract - archiveEntryToTargetFilePathToCopy.size.toDouble()) / numOfFileNodesToExtract - - val targetPath = archiveEntryToTargetFilePathToCopy[archiveEntry.name]!! - Files.newOutputStream(targetPath, WRITE, CREATE_NEW, TRUNCATE_EXISTING).use { outputStream -> - IOUtils.copy(archiveInputStream(), outputStream) - } - archiveEntryToTargetFilePathToCopy.remove(archiveEntry.name) - if (extractionContext.preserveFileAttributes) { - restoreFileAttributes(archiveEntry, targetPath) - } - } - archiveEntryToTargetFilePathToCopy.isNotEmpty() - } - if (archiveEntryToTargetFilePathToCopy.isNotEmpty()) { - throw IllegalStateException("Unable to find archive entries: ${archiveEntryToTargetFilePathToCopy.map { it.key }.joinToString(", ")}") - } - } - - override fun onThrowable(error: Throwable) { - log.warn("Extraction failed", error) - ApplicationManager.getApplication().invokeLater { - Messages.showErrorDialog(project, error.message, "Extraction Failed") - } - } - - override fun onSuccess() { - if (extractionContext.openTargetDirectoryAfterExtraction) { - BrowserUtil.browse(extractionContext.targetDirectoryPath) - } - else { - notifyAboutExtractionResult(extractionContext) - } - } - - private fun prepareDirectoryNode( - directoryNode: DirectoryNode, - baseDirectoryPath: Path, - extractionContext: ExtractionContext - ): Map { - val directoryPath = if (extractionContext.preserveDirectoryStructure) { - val relativeDirectoryPath = if (extractionContext.createParentDirectories) { - Paths.get(directoryNode.relativePath) - } - else { - Path.of(directoryNode.fileName) - } - val directoryPath = Files.createDirectories(baseDirectoryPath.resolve(relativeDirectoryPath)) - if (extractionContext.preserveFileAttributes && directoryNode.archiveEntry != null) { - restoreFileAttributes(directoryNode.archiveEntry!!, directoryPath) - } - directoryPath - } - else { - baseDirectoryPath - } - - val preparedFileNodeExtractions = mutableMapOf() - directoryNode.children.forEach { archiveNode -> - when (archiveNode) { - is DirectoryNode -> preparedFileNodeExtractions.putAll( - prepareDirectoryNode(archiveNode, directoryPath, extractionContext) - ) - - is FileNode -> { - val filePath = directoryPath.resolve(archiveNode.fileName) - preparedFileNodeExtractions[archiveNode.archiveEntry!!.name] = filePath - } - - else -> throw IllegalStateException("Unknown archive node type: ${archiveNode::class.qualifiedName}") - } - } - return preparedFileNodeExtractions - } - - /** - * A [FileNode] is "standalone" if its parent [DirectoryNode] is not - * extracted. This method creates the associated directory structure mmit. - */ - private fun prepareStandaloneFileNodeExtraction( - fileNode: FileNode, - extractionContext: ExtractionContext - ): Pair { - val filePath = if (extractionContext.preserveDirectoryStructure && extractionContext.createParentDirectories) { - val targetPath = extractionContext.targetDirectoryPath.resolve(Paths.get(fileNode.relativePath)) - Files.createDirectories(targetPath.parent) - targetPath - } - else { - extractionContext.targetDirectoryPath.resolve(Paths.get(fileNode.fileName)) - } - - return fileNode.archiveEntry!! to filePath - } - - private fun notifyAboutExtractionResult(extractionContext: ExtractionContext) { - val numOfExtractedEntries = extractionContext.archiveNodes.size - NotificationUtils.notifyOnToolWindow( - message = "$numOfExtractedEntries ${if (numOfExtractedEntries == 1) "entry" else "entries"} have been extracted to: ${extractionContext.targetDirectoryPath}", - project = project, - notificationType = NotificationType.INFORMATION, - object : DumbAwareAction("Open Target Directory") { - - override fun actionPerformed(e: AnActionEvent) { - BrowserUtil.browse(extractionContext.targetDirectoryPath) - } - } - ) - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - data class OpenUnarchiverContext(val archiveFilePath: Path) : OpenDeveloperToolContext - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Unarchiver", - contentTitle = CONTENT_TITLE - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> Unarchiver) = { - assert(ID == context.id) - Unarchiver(project, it, parentDisposable) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val ID = "unarchiver" - private const val CONTENT_TITLE = "Unarchiver" - - private val DEFAULT_SORTING_MODE = SortingMode.FILENAME_ASC - private const val DEFAULT_SHOW_ARCHIVE_NODE_SIZE = true - private const val DEFAULT_SHOW_ARCHIVE_NODE_TOTAL_NUMBER_OF_CHILDREN = true - private const val DEFAULT_OPEN_IN_EDITOR_ON_DOUBLE_CLICK = true - private val DEFAULT_LAST_OPENED_DIRECTORY_PATH = SystemProperties.getUserHome() - private val DEFAULT_LAST_SELECTED_TARGET_DIRECTORY_PATH = SystemProperties.getUserHome() - private const val DEFAULT_CREATE_ARCHIVE_FILENAME_SUB_DIRECTORY = true - private const val DEFAULT_CLEAR_TARGET_DIRECTORY = false - private const val DEFAULT_PRESERVE_DIRECTORY_STRUCTURE = true - private const val DEFAULT_CREATE_PARENT_DIRECTORIES = true - private const val DEFAULT_PRESERVE_FILE_ATTRIBUTES = true - private const val DEFAULT_OPEN_TARGET_DIRECTORY_AFTER_EXTRACTION = true - - private val log = logger() - - val openUnarchiverReference = OpenDeveloperToolReference.of(ID, OpenUnarchiverContext::class) - - private val supportedArchiveExtensions: Array = - // Keep in sync with `org.apache.commons.compress.compressors.gzip.GzipUtils` - setOf("tgz", "taz", "svgz", "cpgz", "wmz", "emz", "gz", "z") - .plus(ArchiveStreamFactory.findAvailableArchiveInputStreamProviders().map { it.key.lowercase() }) - .sorted() - .toTypedArray() - - private fun restoreFileAttributes(archiveEntry: ArchiveEntry, path: Path) { - val attributes = Files.getFileAttributeView(path, BasicFileAttributeView::class.java) - val oldAttributes = attributes.readAttributes() - - val fileTimes = FileTimes.fromArchiveEntry(archiveEntry) - attributes.setTimes( - fileTimes.lastModifiedTime ?: oldAttributes.lastModifiedTime(), - fileTimes.lastAccessTime ?: oldAttributes.lastAccessTime(), - fileTimes.creationTime ?: oldAttributes.creationTime() - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/HashingTransformer.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/HashingTransformer.kt deleted file mode 100644 index 38bdad94..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/HashingTransformer.kt +++ /dev/null @@ -1,91 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.bindItem -import dev.turingcomplete.intellijdevelopertoolsplugin.common.toHexString -import dev.turingcomplete.intellijdevelopertoolsplugin.common.toMessageDigest -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import java.security.Security - -internal class HashingTransformer( - context: DeveloperUiToolContext, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - project: Project? -) : TextTransformer( - textTransformerContext = TextTransformerContext( - transformActionTitle = "Hash", - sourceTitle = "Plain", - resultTitle = "Hashed" - ), - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var selectedAlgorithm = configuration.register("algorithm", DEFAULT_ALGORITHM) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - check(messageDigestAlgorithms.isNotEmpty()) - - // Validate if selected algorithm is still available - if (messageDigestAlgorithms.find { it == selectedAlgorithm.get() } == null) { - selectedAlgorithm.set(messageDigestAlgorithms.find { it == DEFAULT_ALGORITHM } ?: messageDigestAlgorithms.first()) - } - } - - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun transform() { - val hash = selectedAlgorithm.get().toMessageDigest().digest(sourceText.get().encodeToByteArray()) - resultText.set(hash.toHexString()) - } - - override fun Panel.buildTopConfigurationUi() { - row { - comboBox(messageDigestAlgorithms) - .label("Algorithm:") - .bindItem(selectedAlgorithm) - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Hashing", - contentTitle = "Hashing Transformer" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> HashingTransformer)? { - if (messageDigestAlgorithms.isEmpty()) { - return null - } - - return { configuration -> HashingTransformer(context, configuration, parentDisposable, project) } - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val DEFAULT_ALGORITHM = "SHA-256" - private val messageDigestAlgorithms: List by lazy { Security.getAlgorithms("MessageDigest").sorted() } - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/HmacTransformer.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/HmacTransformer.kt deleted file mode 100644 index f08a0834..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/HmacTransformer.kt +++ /dev/null @@ -1,208 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer - -import com.intellij.icons.AllIcons -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.actionButton -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.whenItemSelectedFromUi -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.SimpleToggleAction -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.registerDynamicToolTip -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateNonEmpty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.HmacTransformer.SecretKeyEncodingMode.BASE32 -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.HmacTransformer.SecretKeyEncodingMode.BASE64 -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.HmacTransformer.SecretKeyEncodingMode.RAW -import dev.turingcomplete.intellijdevelopertoolsplugin.common.toHexString -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.CONFIGURATION -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.SENSITIVE -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import io.ktor.util.* -import org.apache.commons.codec.binary.Base32 -import java.security.Security -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec - -internal class HmacTransformer( - context: DeveloperUiToolContext, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - project: Project? -) : TextTransformer( - textTransformerContext = TextTransformerContext( - transformActionTitle = "Generate", - sourceTitle = "Data", - resultTitle = "Hash" - ), - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var selectedAlgorithm = configuration.register("algorithm", DEFAULT_ALGORITHM) - - private val secretKey = configuration.register("secretKey", SECRET_KEY_DEFAULT, SENSITIVE, EXAMPLE_SECRET) - private val secretKeyEncodingMode = configuration.register("secretKeyEncodingMode", RAW, CONFIGURATION) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - check(algorithms.isNotEmpty()) - - secretKey.afterChange { - if (!isDisposed && liveTransformation.get()) { - transform() - } - } - - secretKeyEncodingMode.afterChange { - if (!isDisposed && liveTransformation.get()) { - transform() - } - } - - // Validate if selected algorithm is still available - val selectedAlgorithm = selectedAlgorithm.get() - if (algorithms.find { it.algorithm == selectedAlgorithm } == null) { - this.selectedAlgorithm.set((algorithms.find { it.algorithm.equals(DEFAULT_ALGORITHM, true) } ?: algorithms.first()).algorithm) - } - } - - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun transform() { - if (validate().isNotEmpty()) { - return - } - - val secretKeyValue = secretKey.get() - if (secretKeyValue.isEmpty()) { - resultText.set("") - return - } - - val hmac: ByteArray = Mac.getInstance(selectedAlgorithm.get()).run { - val secretKey = when (secretKeyEncodingMode.get()) { - RAW -> secretKeyValue - BASE32 -> Base32().decode(secretKeyValue).decodeToString() - BASE64 -> secretKeyValue.decodeBase64String() - } - init(SecretKeySpec(secretKey.encodeToByteArray(), selectedAlgorithm.get())) - doFinal(sourceText.get().encodeToByteArray()) - } - resultText.set(hmac.toHexString()) - } - - @Suppress("UnstableApiUsage") - override fun Panel.buildTopConfigurationUi() { - row { - comboBox(algorithms) - .label("Algorithm:") - .applyToComponent { selectedItem = algorithms.find { it.algorithm == selectedAlgorithm.get() } } - .whenItemSelectedFromUi { selectedAlgorithm.set(it.algorithm) } - } - } - - override fun Panel.buildMiddleConfigurationUi() { - row { - expandableTextField() - .label("Secret key:") - .align(AlignX.FILL) - .bindText(secretKey) - .validateNonEmpty("A secret key must be provided") - .gap(RightGap.SMALL) - .resizableColumn() - .registerDynamicToolTip { DeveloperToolsApplicationSettings.instance.createSensitiveInputsHandlingToolTipText() } - - val encodingActions = mutableListOf().apply { - SecretKeyEncodingMode.entries.forEach { secretKeyEncodingModeValue -> - add(SimpleToggleAction( - text = secretKeyEncodingModeValue.title, - icon = AllIcons.Actions.ToggleSoftWrap, - isSelected = { secretKeyEncodingMode.get() == secretKeyEncodingModeValue }, - setSelected = { - secretKeyEncodingMode.set(secretKeyEncodingModeValue) - } - )) - } - } - actionButton(UiUtils.actionsPopup( - title = "Encoding", - icon = AllIcons.General.Settings, - actions = encodingActions - )) - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private data class HmacAlgorithm(val title: String, val algorithm: String) { - - override fun toString(): String = title - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - enum class SecretKeyEncodingMode(val title: String) { - - RAW("Raw"), - BASE32("Base32 Encoded"), - BASE64("Base64 Encoded") - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "HMAC", - contentTitle = "HMAC Transformer" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> HmacTransformer)? { - if (algorithms.isEmpty()) { - return null - } - - return { configuration -> HmacTransformer(context, configuration, parentDisposable, project) } - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val DEFAULT_ALGORITHM = "HMACSHA256" - private const val SECRET_KEY_DEFAULT = "" - - private val algorithms: List by lazy { - Security.getAlgorithms("Mac") - .asSequence() - .filter { it.startsWith("HMAC") } - .filter { - // Would require a complex PBEKey - !it.contains("PBE") - } - .map { HmacAlgorithm(it.replace("HMAC", "Hmac"), it) } - .sortedBy { it.title } - .toList() - } - - private const val EXAMPLE_SECRET = "s3cre!" - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/SqlFormattingTransformer.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/SqlFormattingTransformer.kt deleted file mode 100644 index a3354348..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/SqlFormattingTransformer.kt +++ /dev/null @@ -1,142 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer - -import com.github.vertical_blank.sqlformatter.SqlFormatter -import com.github.vertical_blank.sqlformatter.core.FormatConfig -import com.github.vertical_blank.sqlformatter.languages.Dialect -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindSelected -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bindIntTextImproved -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.validateLongValue -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -class SqlFormattingTransformer( - context: DeveloperUiToolContext, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - project: Project? -) : TextTransformer( - textTransformerContext = TextTransformerContext( - transformActionTitle = "Format", - sourceTitle = "Plain SQL", - resultTitle = "Formatted SQL", - initialSourceExampleText = EXAMPLE_SQL, - diffSupport = DiffSupport( - title = "SQL Formatting" - ) - ), - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -), DeveloperToolConfiguration.ChangeListener { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var dialect = configuration.register("dialect", DEFAULT_DIALECT) - private var indentSpaces = configuration.register("indentSpaces", DEFAULT_INDENT_SPACES) - private var uppercase = configuration.register("uppercase", DEFAULT_UPPERCASE) - private var linesBetweenQueries = configuration.register("linesBetweenQueries", DEFAULT_LINES_BETWEEN_QUERIES) - private var maxColumnLength = configuration.register("maxColumnLength", DEFAULT_MAX_COLUMN_LENGTH) - - private lateinit var formatConfig: FormatConfig - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun Panel.buildMiddleConfigurationUi() { - row { - comboBox(Dialect.entries) - .label("Dialect:") - .bindItem(dialect) - }.layout(RowLayout.PARENT_GRID) - - row { - textField() - .label("Indent spaces:") - .bindIntTextImproved(indentSpaces) - .validateLongValue(LongRange(0, 99)) - }.layout(RowLayout.PARENT_GRID) - - row { - textField() - .label("Lines between queries:") - .bindIntTextImproved(linesBetweenQueries) - .validateLongValue(LongRange(0, 99)) - }.layout(RowLayout.PARENT_GRID) - - row { - textField() - .label("Maximum column length:") - .bindIntTextImproved(maxColumnLength) - .validateLongValue(LongRange(0, 99)) - }.layout(RowLayout.PARENT_GRID) - - row { - checkBox("Convert keywords to uppercase") - .bindSelected(uppercase) - }.layout(RowLayout.PARENT_GRID) - } - - override fun afterBuildUi() { - updateFormatConfig() - super.afterBuildUi() - } - - override fun transform() { - resultText.set(SqlFormatter.of(dialect.get()).format(sourceText.get(), formatConfig)) - } - - override fun configurationChanged(property: ValueProperty) { - updateFormatConfig() - super.configurationChanged(property) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun updateFormatConfig() { - formatConfig = FormatConfig.builder() - .indent(" ".repeat(indentSpaces.get())) - .uppercase(uppercase.get()) - .linesBetweenQueries(linesBetweenQueries.get()) - .maxColumnLength(maxColumnLength.get()) - .build() - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "SQL Formatting", - contentTitle = "SQL Formatting" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> SqlFormattingTransformer) = { configuration -> - SqlFormattingTransformer(context, configuration, parentDisposable, project) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val EXAMPLE_SQL = "select c.id, c.name, o.address, o.orderedAt from customers c left join orders o ON (o.customerId = c.id) order by o.orderedAt" - - private val DEFAULT_DIALECT = Dialect.N1ql - private const val DEFAULT_INDENT_SPACES = 2 - private const val DEFAULT_UPPERCASE = true - private const val DEFAULT_LINES_BETWEEN_QUERIES = 1 - private const val DEFAULT_MAX_COLUMN_LENGTH = 30 - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextCaseTransformer.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextCaseTransformer.kt deleted file mode 100644 index 42eef9e0..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextCaseTransformer.kt +++ /dev/null @@ -1,174 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.selected -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bind -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextCaseTransformer.OriginalParsingMode.AUTOMATIC_DETECTION -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextCaseTransformer.OriginalParsingMode.FIXED_TEXT_CASE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextCaseTransformer.OriginalParsingMode.INDIVIDUAL_DELIMITER -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextCaseTransformer.TextCase.COBOL_CASE -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextCaseTransformer.TextCase.STRICT_CAMEL_CASE -import dev.turingcomplete.intellijdevelopertoolsplugin.common.TextCaseUtils -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation -import dev.turingcomplete.textcaseconverter.StandardTextCases -import dev.turingcomplete.textcaseconverter.toTextCase -import dev.turingcomplete.textcaseconverter.toWordsSplitter -import dev.turingcomplete.textcaseconverter.TextCase as StandardTextCase - -class TextCaseTransformer( - context : DeveloperUiToolContext, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - project: Project? -) : TextTransformer( - textTransformerContext = TextTransformerContext( - transformActionTitle = "Transform", - sourceTitle = "Original", - resultTitle = "Target", - initialSourceExampleText = EXAMPLE_INPUT_TEXT, - diffSupport = DiffSupport( - title = "Text Case Transformer" - ), - supportsDebug = true - ), - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var originalParsingMode = configuration.register("originalParsingMode", AUTOMATIC_DETECTION) - private var individualDelimiter = configuration.register("individualDelimiter", " ") - private var inputTextCase = configuration.register("inputTextCase", STRICT_CAMEL_CASE) - private var outputTextCase = configuration.register("outputTextCase", COBOL_CASE) - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun transform() { - resultText.set(sourceText.get().toTextCase(outputTextCase.get().textCase, getInputWordsSplitter())) - } - - override fun Panel.buildMiddleConfigurationUi() { - buttonsGroup("Original:") { - row { - radioButton("Automatic detection") - .bind(originalParsingMode, AUTOMATIC_DETECTION) - } - - row { - val fixedTextCaseRadioButton = radioButton("Fixed text case:") - .bind(originalParsingMode, FIXED_TEXT_CASE) - .gap(RightGap.SMALL) - comboBox(TextCase.entries) - .bindItem(inputTextCase) - .enabledIf(fixedTextCaseRadioButton.selected).component - } - - row { - val individualDelimiterRadioButton = radioButton("Split words by:") - .bind(originalParsingMode, INDIVIDUAL_DELIMITER) - .gap(RightGap.SMALL) - textField() - .bindText(individualDelimiter) - .enabledIf(individualDelimiterRadioButton.selected).component - } - } - - row { - comboBox(TextCase.entries) - .label("Target:") - .bindItem(outputTextCase) - } - } - - override fun Panel.buildDebugComponent() { - group("Input Words") { - row { - val inputWords = getInputWordsSplitter().split(sourceText.get()) - if (inputWords.isEmpty()) { - label("None") - } - else { - val wordsList = inputWords.joinToString(separator = "", prefix = "
      ", postfix = "
    ") { "
  • $it
  • " } - label("$wordsList") - } - } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun getInputWordsSplitter() = - when (originalParsingMode.get()) { - AUTOMATIC_DETECTION -> TextCaseUtils.determineWordsSplitter(sourceText.get(), inputTextCase.get().textCase) - FIXED_TEXT_CASE -> inputTextCase.get().textCase.wordsSplitter() - INDIVIDUAL_DELIMITER -> individualDelimiter.get().toWordsSplitter() - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class TextCase(val textCase: StandardTextCase) { - - SCREAMING_SNAKE_CASE(StandardTextCases.SCREAMING_SNAKE_CASE), - SOFT_CAMEL_CASE(StandardTextCases.SOFT_CAMEL_CASE), - STRICT_CAMEL_CASE(StandardTextCases.STRICT_CAMEL_CASE), - PASCAL_CASE(StandardTextCases.PASCAL_CASE), - SNAKE_CASE(StandardTextCases.SNAKE_CASE), - KEBAB_CASE(StandardTextCases.KEBAB_CASE), - TRAIN_CASE(StandardTextCases.TRAIN_CASE), - COBOL_CASE(StandardTextCases.COBOL_CASE), - PASCAL_SNAKE_CASE(StandardTextCases.PASCAL_SNAKE_CASE), - CAMEL_SNAKE_CASE(StandardTextCases.CAMEL_SNAKE_CASE), - LOWER_CASE(StandardTextCases.LOWER_CASE), - UPPER_CASE(StandardTextCases.UPPER_CASE), - INVERTED_CASE(StandardTextCases.INVERTED_CASE), - ALTERNATING_CASE(StandardTextCases.ALTERNATING_CASE), - DOT_CASE(StandardTextCases.DOT_CASE); - - override fun toString(): String = "${textCase.title()} (${textCase.example()})" - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class OriginalParsingMode { - - AUTOMATIC_DETECTION, - FIXED_TEXT_CASE, - INDIVIDUAL_DELIMITER - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Text Case", - contentTitle = "Text Case Transformer" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> TextCaseTransformer) = { configuration -> - TextCaseTransformer(context, configuration, parentDisposable, project) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - const val EXAMPLE_INPUT_TEXT = "thisIsAnExampleText" - } -} diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextFilterTransformer.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextFilterTransformer.kt deleted file mode 100644 index 0160b905..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/transformer/TextFilterTransformer.kt +++ /dev/null @@ -1,216 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.RowLayout -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindItem -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.selected -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ErrorHolder -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.bind -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.regex.RegexTextField -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.regex.SelectRegexOptionsAction -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration.PropertyType.INPUT -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolPresentation - -class TextFilterTransformer( - context: DeveloperUiToolContext, - configuration: DeveloperToolConfiguration, - parentDisposable: Disposable, - project: Project? -) : TextTransformer( - textTransformerContext = TextTransformerContext( - transformActionTitle = "Filter", - sourceTitle = "Unfiltered", - resultTitle = "Filtered", - initialSourceExampleText = EXAMPLE_INPUT, - diffSupport = DiffSupport( - title = "Text Filter" - ) - ), - context = context, - configuration = configuration, - parentDisposable = parentDisposable, - project = project -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val tokenMode = configuration.register("tokenSelectionMode", DEFAULT_TOKEN_SELECTION_MODE) - private val filteringMode = configuration.register("filteringMode", DEFAULT_FILTERING_MODE) - private val filteringContainingModeText = configuration.register("filteringContainingModeText", "", INPUT, EXAMPLE_FILTERING_CONTAINING_MODE_TEXT) - private val filteringNotContainingModeText = configuration.register("filteringNotContainingModeText", "", INPUT, EXAMPLE_FILTERING_NOT_CONTAINING_MODE_TEXT) - private val filteringRegexModeText = configuration.register("filteringRegexModeText", "", INPUT, EXAMPLE_FILTERING_REGEX_MODE_TEXT) - private val filteringRegexModeOptions = configuration.register("filteringRegexModeOptions", 0) - private val filteringRegexModeErrorHolder = ErrorHolder() - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - override fun Panel.buildMiddleConfigurationUi() { - row { - comboBox(TokenMode.entries) - .label("Filter:") - .bindItem(tokenMode) - }.layout(RowLayout.PARENT_GRID).topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - buttonsGroup { - row { - cell() - val containingFilteringModeRadioButton = radioButton("Containing:") - .bind(filteringMode, FilteringMode.CONTAINING) - .gap(RightGap.SMALL) - expandableTextField() - .bindText(filteringContainingModeText) - .enabledIf(containingFilteringModeRadioButton.selected) - .resizableColumn() - .align(Align.FILL) - }.layout(RowLayout.PARENT_GRID).bottomGap(BottomGap.NONE) - - row { - cell() - val containingFilteringModeRadioButton = radioButton("Not containing:") - .bind(filteringMode, FilteringMode.NOT_CONTAINING) - .gap(RightGap.SMALL) - expandableTextField() - .bindText(filteringNotContainingModeText) - .enabledIf(containingFilteringModeRadioButton.selected) - .resizableColumn() - .align(Align.FILL) - }.layout(RowLayout.PARENT_GRID).topGap(TopGap.NONE).bottomGap(BottomGap.NONE) - - row { - cell() - radioButton("Matching regular expression:") - .bind(filteringMode, FilteringMode.REGEX) - .gap(RightGap.SMALL) - cell(RegexTextField(project, parentDisposable, filteringRegexModeText)) - .validationOnApply(filteringRegexModeErrorHolder.asValidation()) - .validationRequestor(DUMMY_DIALOG_VALIDATION_REQUESTOR) - .resizableColumn() - .align(Align.FILL) - .gap(RightGap.SMALL) - cell(SelectRegexOptionsAction.createActionButton(filteringRegexModeOptions)) - }.layout(RowLayout.PARENT_GRID).topGap(TopGap.NONE) - } - } - - override fun transform() { - val tokenFilter: (String) -> Boolean = when (filteringMode.get()) { - FilteringMode.CONTAINING -> { - val filteringContainingModeTextValue = filteringContainingModeText.get() - ({ it.contains(filteringContainingModeTextValue) }) - } - - FilteringMode.NOT_CONTAINING -> { - val filteringNotContainingModeTextValue = filteringNotContainingModeText.get() - ({ !it.contains(filteringNotContainingModeTextValue) }) - } - - FilteringMode.REGEX -> { - val filteringRegexModeTextValue = try { - Regex(filteringRegexModeText.get()) - } catch (e: Exception) { - filteringRegexModeErrorHolder.add(e) - return - } - filteringRegexModeTextValue::matches - } - } - - val result = when (tokenMode.get()) { - TokenMode.WORD -> { - with(StringBuilder()) { - var lastWasWord = false - for (match in WORDS_SPLIT_REGEX.findAll(sourceText.get())) { - val token = match.value - if (token.isBlank() && !token.contains("\n")) { - if (lastWasWord) { - append(token) - } - } else if (token.contains("\n")) { - append(token) - lastWasWord = false - } else if (tokenFilter(token)) { - append(token) - lastWasWord = true - } else { - lastWasWord = false - } - } - toString().trim() - } - } - - TokenMode.LINE -> sourceText.get() - .lines() - .filter(tokenFilter) - .joinToString(System.lineSeparator()) - } - resultText.set(result) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class TokenMode(val pluralTitle: String) { - - WORD("Words"), - LINE("Lines"); - - override fun toString(): String = pluralTitle - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private enum class FilteringMode { - - CONTAINING, - NOT_CONTAINING, - REGEX - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - class Factory : DeveloperUiToolFactory { - - override fun getDeveloperUiToolPresentation() = DeveloperUiToolPresentation( - menuTitle = "Text Filter", - contentTitle = "Text Filter" - ) - - override fun getDeveloperUiToolCreator( - project: Project?, - parentDisposable: Disposable, - context: DeveloperUiToolContext - ): ((DeveloperToolConfiguration) -> TextFilterTransformer) = { configuration -> - TextFilterTransformer(context, configuration, parentDisposable, project) - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val WORDS_SPLIT_REGEX = Regex("(\\S+|\\s+)") - - private val DEFAULT_TOKEN_SELECTION_MODE = TokenMode.LINE - private val DEFAULT_FILTERING_MODE = FilteringMode.CONTAINING - - private const val EXAMPLE_FILTERING_CONTAINING_MODE_TEXT = "[error]" - private const val EXAMPLE_FILTERING_NOT_CONTAINING_MODE_TEXT = "[info]" - private const val EXAMPLE_FILTERING_REGEX_MODE_TEXT = "^\\[error\\].*$" - - private val EXAMPLE_INPUT = """ - [info] Application started - [error] Error occurred while processing request - """.trimIndent() - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/AddOpenMainDialogActionToMainToolbarTask.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/AddOpenMainDialogActionToMainToolbarTask.kt deleted file mode 100644 index db9508a9..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/AddOpenMainDialogActionToMainToolbarTask.kt +++ /dev/null @@ -1,71 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui - -import com.intellij.openapi.actionSystem.ActionManager -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.Anchor -import com.intellij.openapi.actionSystem.Constraints -import com.intellij.openapi.actionSystem.DefaultActionGroup -import com.intellij.openapi.actionSystem.IdeActions -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.dialog.OpenMainDialogAction -import dev.turingcomplete.intellijdevelopertoolsplugin.common.safeCastTo - -class AddOpenMainDialogActionToMainToolbarTask( - private val openMainDialogAction: AnAction, - private val mainToolbarRightActionGroup: DefaultActionGroup?, - private val mainToolbarActionGroup: DefaultActionGroup? -) : Runnable { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun run() { - if (mainToolbarRightActionGroup != null && !mainToolbarRightActionGroup.containsAction(openMainDialogAction)) { - mainToolbarRightActionGroup.add(openMainDialogAction, Constraints(Anchor.BEFORE, "SearchEverywhere")) - } - - if (mainToolbarActionGroup != null && !mainToolbarActionGroup.containsAction(openMainDialogAction)) { - mainToolbarActionGroup.add(openMainDialogAction, Constraints.LAST) - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - /** - * Notes regarding the already added check: - * - The [DefaultActionGroup.add] in [AddOpenMainDialogActionToMainToolbarTask] - * will not add the action if the toolbar does not exist. Each toolbar - * exists only either in the new or old UI. So, the action will only be - * added to the toolbar of the currently active UI. - * - If the user activity removed the action from the toolbar, the - * [AddOpenMainDialogActionToMainToolbarTask] will still add the action, - * but it will be hidden (this due to the internal working toolbar - * action mechanism). Therefore, the [DefaultActionGroup.containsAction] - * returns true even though the action is not visible. - */ - fun createIfAvailable(): AddOpenMainDialogActionToMainToolbarTask? { - val openMainDialogAction = OpenMainDialogAction.getAction() ?: return null - - val mainToolbarRightActionGroup = ActionManager.getInstance().getAction(IdeActions.GROUP_MAIN_TOOLBAR_RIGHT)?.safeCastTo() // New UI - val mainToolbarActionGroup = ActionManager.getInstance().getAction(IdeActions.GROUP_MAIN_TOOLBAR)?.safeCastTo() // Old UI - if (mainToolbarRightActionGroup == null && mainToolbarActionGroup == null) { - return null - } - if ( - mainToolbarRightActionGroup?.containsAction(openMainDialogAction) == true - || mainToolbarActionGroup?.containsAction(openMainDialogAction) == true - ) { - return null - } - - return AddOpenMainDialogActionToMainToolbarTask( - openMainDialogAction = openMainDialogAction, - mainToolbarRightActionGroup = mainToolbarRightActionGroup, - mainToolbarActionGroup = mainToolbarActionGroup - ) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/ChangelogDialog.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/ChangelogDialog.kt deleted file mode 100644 index eea5ccf3..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/ChangelogDialog.kt +++ /dev/null @@ -1,67 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui - -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.DialogWrapper -import com.intellij.ui.ScrollPaneFactory -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.panel -import com.intellij.util.ui.UIUtil -import com.intellij.util.ui.components.BorderLayoutPanel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.UiUtils -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.ToolsMenuTree -import javax.swing.Action -import javax.swing.JComponent - -class ChangelogDialog( - project: Project?, - parentComponent: JComponent -) : DialogWrapper( - project, - parentComponent, - true, - IdeModalityType.IDE -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - title = "Developer Tools - What's New" - setSize(600, 650) - isModal = true - init() - } - - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun createCenterPanel(): JComponent = BorderLayoutPanel(0, UIUtil.LARGE_VGAP).apply { - addToCenter(panel { - row { - text(ToolsMenuTree::class.java.getResource(CHANGELOG_HTML_FILE)?.readText() ?: "Couldn't find 'What's New' text") - .resizableColumn() - .align(Align.FILL) - }.resizableRow() - }.let { ScrollPaneFactory.createScrollPane(it, true) }) - - addToBottom( - BorderLayoutPanel().apply { - addToLeft( - UiUtils.createLink( - title = "Make a feature request or report an issue", - url = "https://github.com/marcelkliemannel/intellij-developer-tools-plugin/issues" - ) - ) - } - ) - } - - override fun createActions(): Array = arrayOf(myOKAction) - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private const val CHANGELOG_HTML_FILE = "/dev/turingcomplete/intellijdevelopertoolsplugin/changelog.html" - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/ContentPanelHandler.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/ContentPanelHandler.kt deleted file mode 100644 index 7b72102c..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/ContentPanelHandler.kt +++ /dev/null @@ -1,161 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.content - -import com.intellij.openapi.Disposable -import com.intellij.openapi.project.Project -import com.intellij.ui.EditorNotificationPanel -import com.intellij.ui.components.ActionLink -import com.intellij.util.ui.components.BorderLayoutPanel -import com.intellij.util.ui.tree.TreeUtil -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.ValueProperty -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsInstanceSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.AddOpenMainDialogActionToMainToolbarTask -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling.OpenDeveloperToolReference -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.ContentNode -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.DeveloperToolNode -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.GroupNode -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.ToolsMenuTree -import javax.swing.JPanel - -internal open class ContentPanelHandler( - protected val project: Project?, - protected val parentDisposable: Disposable, - settings: DeveloperToolsInstanceSettings, - groupNodeSelectionEnabled: Boolean = true, - promoteMainDialog: Boolean = false, - prioritizeVerticalLayout: Boolean = false -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private var selectedContentNode = ValueProperty(null) - - val contentPanel = BorderLayoutPanel() - val toolsMenuTree: ToolsMenuTree - - protected val innerContentPanel = BorderLayoutPanel() - private val mainDialogPromotionPanel = BorderLayoutPanel() - - private val cachedGroupsPanels = mutableMapOf() - private val cachedDeveloperToolsPanels = mutableMapOf() - - // -- Initialization ---------------------------------------------------------------------------------------------- // - - init { - // The creation of `ToolsMenuTree` will trigger the initial node selection - toolsMenuTree = ToolsMenuTree( - project, - parentDisposable, - settings, - groupNodeSelectionEnabled, - prioritizeVerticalLayout - ) { node, selectionTriggeredBySearch -> handleContentNodeSelection(node, selectionTriggeredBySearch) } - - initMainDialogPromotionPanel(promoteMainDialog) - - contentPanel.apply { - addToTop(mainDialogPromotionPanel) - addToCenter(innerContentPanel) - } - } - - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - fun openTool(context: T, reference: OpenDeveloperToolReference) { - toolsMenuTree.selectDeveloperTool(reference.id) { - cachedDeveloperToolsPanels[selectedContentNode.get()]?.openTool(context, reference) - } - } - - fun showTool(id: String) { - toolsMenuTree.selectDeveloperTool(id) {} - } - - protected open fun createDeveloperToolContentPanel(developerToolNode: DeveloperToolNode): DeveloperToolContentPanel = - DeveloperToolContentPanel(developerToolNode) - - protected open fun handleContentNodeSelection(new: ContentNode?, selectionTriggeredBySearch: Boolean) { - val old = selectedContentNode.get() - if (old != new) { - if (old is DeveloperToolNode) { - cachedDeveloperToolsPanels[old]?.deselected() - } - - if (new == null) { - return - } - - when (new) { - is GroupNode -> { - setContentPanel(cachedGroupsPanels.getOrPut(new.developerUiToolGroup.id) { - GroupContentPanel(new) { - selectedContentNode.set(it) - TreeUtil.selectNode(toolsMenuTree, it) - } - }.panel) - selectedContentNode.set(new) - } - - is DeveloperToolNode -> { - setContentPanel(cachedDeveloperToolsPanels.getOrPut(new) { createDeveloperToolContentPanel(new) }.also { it.selected() }) - selectedContentNode.set(new) - } - - else -> error("Unexpected menu node: ${new::class}") - } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun initMainDialogPromotionPanel(promoteMainDialog: Boolean) { - if (!promoteMainDialog) { - return - } - - if (!DeveloperToolsApplicationSettings.instance.promoteAddOpenMainDialogActionToMainToolbar) { - return - } - - val addOpenMainDialogActionToMainToolbarTask = AddOpenMainDialogActionToMainToolbarTask.createIfAvailable() - ?: return - - val promoteMainDialogNotificationPanel = PromoteMainDialogNotificationPanel { addOpenMainDialogActionToMainToolbar -> - DeveloperToolsApplicationSettings.instance.apply { - this@apply.addOpenMainDialogActionToMainToolbar = addOpenMainDialogActionToMainToolbar - promoteAddOpenMainDialogActionToMainToolbar = false - } - - if (addOpenMainDialogActionToMainToolbar) { - addOpenMainDialogActionToMainToolbarTask.run() - } - - mainDialogPromotionPanel.isVisible = false - } - mainDialogPromotionPanel.addToCenter(promoteMainDialogNotificationPanel) - } - - private fun setContentPanel(nodePanel: JPanel) { - innerContentPanel.apply { - removeAll() - addToCenter(nodePanel) - revalidate() - repaint() - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class PromoteMainDialogNotificationPanel( - closeNotification: (Boolean) -> Unit - ) : EditorNotificationPanel(Status.Promo) { - - init { - text = "The tools are also available as a standalone window." - myLinksPanel.add(ActionLink("Add to main toolbar") { closeNotification(true) }) - setCloseAction { closeNotification(false) } - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/GroupContentPanel.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/GroupContentPanel.kt deleted file mode 100644 index 5bc9796e..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/content/GroupContentPanel.kt +++ /dev/null @@ -1,53 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.content - -import com.intellij.openapi.ui.DialogPanel -import com.intellij.ui.ScrollPaneFactory -import com.intellij.ui.components.ActionLink -import com.intellij.ui.dsl.builder.Align -import com.intellij.ui.dsl.builder.BottomGap -import com.intellij.ui.dsl.builder.panel -import com.intellij.util.ui.JBEmptyBorder -import com.intellij.util.ui.JBFont -import com.intellij.util.ui.UIUtil -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.ContentNode -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.DeveloperToolNode -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.GroupNode -import org.jdesktop.swingx.VerticalLayout -import javax.swing.JPanel -import javax.swing.ScrollPaneConstants - -internal class GroupContentPanel(groupNode: GroupNode, private val onContentNodeSelection: (ContentNode) -> Unit) { - // -- Properties -------------------------------------------------------------------------------------------------- // - - val panel: DialogPanel = panel { - row { - label(groupNode.developerUiToolGroup.detailTitle).applyToComponent { font = JBFont.label().asBold() } - bottomGap(BottomGap.NONE) - } - - indent { - row { - val component = createDeveloperToolLinksPanel(groupNode) - val componentWrapper = ScrollPaneFactory.createScrollPane(component, true).apply { - horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS - verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS - } - cell(componentWrapper).align(Align.FILL) - }.resizableRow() - } - }.apply { border = JBEmptyBorder(0, 8, 0, 8) } - - private fun createDeveloperToolLinksPanel(groupNode: GroupNode) = object : JPanel(VerticalLayout(UIUtil.DEFAULT_VGAP)) { - init { - groupNode.children().asSequence().filterIsInstance(DeveloperToolNode::class.java).forEach { developerToolNode -> - add(ActionLink(developerToolNode.developerUiToolPresentation.groupedMenuTitle) { onContentNodeSelection(developerToolNode) }) - } - } - } - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolContext.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolContext.kt deleted file mode 100644 index 41283a57..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolContext.kt +++ /dev/null @@ -1,10 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling - -interface OpenDeveloperToolContext { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolService.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolService.kt deleted file mode 100644 index e1186b75..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/handling/OpenDeveloperToolService.kt +++ /dev/null @@ -1,50 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.handling - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings.ActionHandlingInstance.TOOL_WINDOW -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.dialog.MainDialogService -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.toolwindow.MainToolWindowService - -@Service(Service.Level.PROJECT) -internal class OpenDeveloperToolService(val project: Project) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - fun openTool(context: T, reference: OpenDeveloperToolReference) { - if (showToolWindow()) { - project.service().openTool(context, reference) - } - else { - ApplicationManager.getApplication().service().openTool(project, context, reference) - } - } - - fun showTool(id: String) { - if (showToolWindow()) { - project.service().showTool(id) - } - else { - ApplicationManager.getApplication().service().showTool(project, id) - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun showToolWindow(): Boolean { - val showToolWindow = if (DeveloperToolsApplicationSettings.instance.autoDetectActionHandlingInstance) { - !DeveloperToolsApplicationSettings.instance.addOpenMainDialogActionToMainToolbar - } - else { - DeveloperToolsApplicationSettings.instance.selectedActionHandlingInstance == TOOL_WINDOW - } - return showToolWindow - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/toolwindow/MainToolWindowFactory.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/toolwindow/MainToolWindowFactory.kt deleted file mode 100644 index f199ee80..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/instance/toolwindow/MainToolWindowFactory.kt +++ /dev/null @@ -1,169 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.toolwindow - -import com.intellij.openapi.Disposable -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.service -import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.popup.JBPopup -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowFactory -import com.intellij.openapi.wm.impl.content.ToolWindowContentUi -import com.intellij.ui.AnimatedIcon -import com.intellij.ui.IconManager -import com.intellij.ui.content.ContentFactory -import com.intellij.ui.dsl.builder.RightGap -import com.intellij.ui.dsl.builder.Row -import com.intellij.ui.dsl.builder.actionButton -import com.intellij.util.ui.components.BorderLayoutPanel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsApplicationSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings.DeveloperToolsToolWindowSettings -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.content.ContentPanelHandler -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.content.DeveloperToolContentPanel -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.instance.toolwindow.MainToolWindowService.Companion.toolWindowContentPanelHandlerKey -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.ContentNode -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu.DeveloperToolNode -import java.awt.Dimension -import javax.swing.JComponent -import javax.swing.JLabel - -internal class MainToolWindowFactory : ToolWindowFactory, DumbAware { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - override fun init(toolWindow: ToolWindow) { - assert(toolWindow.id == ID) - - toolWindow.component.putClientProperty(ToolWindowContentUi.HIDE_ID_LABEL, "false") - toolWindow.stripeTitle = "Developer Tools" - } - - override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val loaderPanel = BorderLayoutPanel().apply { - addToCenter(JLabel(AnimatedIcon.Default.INSTANCE)) - } - toolWindow.contentManager.addContent(ContentFactory.getInstance().createContent(loaderPanel, "", false)) - - ApplicationManager.getApplication().executeOnPooledThread { - // See `DeveloperToolsToolWindowSettings#getInstance` for an explanation - // why the settings must be instantiated on a background thread. - val settings = DeveloperToolsToolWindowSettings.getInstance(project) - - ApplicationManager.getApplication().invokeLater { - toolWindow.contentManager.removeAllContents(true) - - val contentPanelHandler = ToolWindowContentPanelHandler(settings, project, toolWindow.disposable) - val mainContent = ContentFactory.getInstance().createContent(contentPanelHandler.contentPanel, "", false).apply { - preferredFocusableComponent = contentPanelHandler.contentPanel - putUserData(toolWindowContentPanelHandlerKey, contentPanelHandler) - } - toolWindow.contentManager.addContent(mainContent) - toolWindow.contentManager.setSelectedContent(mainContent) - - project.service().setToolWindow(toolWindow) - } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ToolWindowDeveloperToolContentPanel( - developerToolNode: DeveloperToolNode, - private val toggleMenu: (JComponent) -> Unit - ) : DeveloperToolContentPanel(developerToolNode) { - - override fun Row.buildTitle(): JComponent { - val menuIcon = IconManager.getInstance().getIcon("dev/turingcomplete/intellijdevelopertoolsplugin/icons/menu.svg", MainToolWindowFactory::class.java.classLoader) - lateinit var toggleMenuActionLink: JComponent - val toggleMenuAction: DumbAwareAction = object : DumbAwareAction("Show Developer Tool", null, menuIcon) { - - override fun actionPerformed(e: AnActionEvent) { - toggleMenu(toggleMenuActionLink) - } - } - toggleMenuActionLink = actionButton(toggleMenuAction) - .gap(RightGap.SMALL) - .component - - @Suppress("DialogTitleCapitalization") - return label(developerToolNode.developerUiToolPresentation.contentTitle) - .applyToComponent { formatTitle() } - .component - } - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private class ToolWindowContentPanelHandler( - settings: DeveloperToolsToolWindowSettings, - project: Project, - parentDisposable: Disposable - ) : ContentPanelHandler( - project = project, - parentDisposable = parentDisposable, - settings = settings, - groupNodeSelectionEnabled = false, - promoteMainDialog = true, - prioritizeVerticalLayout = true - ) { - - private var lastToolsMenuTreePopup: JBPopup? = null - private var toolsMenuTreeWrapper: JComponent? - private var lastApplicationSettingsModificationsCounter = DeveloperToolsApplicationSettings.instance.modificationCounter - - init { - toolsMenuTreeWrapper = toolsMenuTree.createWrapperComponent(innerContentPanel) - Disposer.register(parentDisposable) { toolsMenuTreeWrapper = null } - } - - override fun createDeveloperToolContentPanel(developerToolNode: DeveloperToolNode): DeveloperToolContentPanel = - ToolWindowDeveloperToolContentPanel(developerToolNode, showMenu()) - - override fun handleContentNodeSelection(new: ContentNode?, selectionTriggeredBySearch: Boolean) { - super.handleContentNodeSelection(new, selectionTriggeredBySearch) - - if (!selectionTriggeredBySearch) { - lastToolsMenuTreePopup?.takeIf { !it.isDisposed }?.cancel() - } - } - - private fun showMenu(): (JComponent) -> Unit = { menuOwner -> - if (lastApplicationSettingsModificationsCounter != DeveloperToolsApplicationSettings.instance.modificationCounter) { - toolsMenuTree.recreateTreeNodes() - lastApplicationSettingsModificationsCounter = DeveloperToolsApplicationSettings.instance.modificationCounter - } - - lastToolsMenuTreePopup = JBPopupFactory.getInstance() - .createComponentPopupBuilder(toolsMenuTreeWrapper!!, toolsMenuTree) - .setRequestFocus(true) - .setResizable(true) - .setMovable(true) - .setDimensionServiceKey(project, TOOLS_MENU_TREE_DIMENSION_SERVICE_KEY, false) - .setCancelOnOtherWindowOpen(true) - .setCancelOnClickOutside(true) - .setMinSize(Dimension(220, 200)) - .createPopup() - .apply { - size = Dimension(220, 600) - Disposer.register(parentDisposable, this) - showUnderneathOf(menuOwner) - } - - } - } - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - const val ID = "Developer Tools" - - private const val TOOLS_MENU_TREE_DIMENSION_SERVICE_KEY = "ToolWindowDeveloperToolContentPanel" - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/GroupNode.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/GroupNode.kt deleted file mode 100644 index 6f18fbbd..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/GroupNode.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu - -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolGroup - -internal class GroupNode(val developerUiToolGroup: DeveloperUiToolGroup) : - ContentNode( - id = developerUiToolGroup.id, - title = developerUiToolGroup.menuTitle - ) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/RootNode.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/RootNode.kt deleted file mode 100644 index e373e99d..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/ui/menu/RootNode.kt +++ /dev/null @@ -1,13 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.ui.menu - -internal class RootNode : ContentNode( - id = "root", - title = "Root" -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolContext.kt b/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolContext.kt deleted file mode 100644 index f8d67a99..00000000 --- a/src/main/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/main/DeveloperUiToolContext.kt +++ /dev/null @@ -1,16 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin.main - -/** - * Context information from the extension point in the `plugin.xml`. - */ -data class DeveloperUiToolContext( - val id: String, - val prioritizeVerticalLayout: Boolean -) { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 6a0d64aa..66a5a2c2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,12 +1,11 @@ - - dev.turingcomplete.intellijdevelopertoolsplugins - Developer Tools - - Marcel Kliemannel - - - Developer Tools
    + + Marcel Kliemannel + + + This plugin is a powerful and versatile set of tools designed to enhance the development experience for software engineers. With its extensive collection of features, developers can increase their productivity and simplify complex operations without leaving their coding environment.

    Plugin icon by Gabriele Malaspina.

    @@ -56,288 +55,406 @@

    All inputs and configurations of the dialog will be stored on the application level.

    ]]>
    - com.intellij.modules.platform - com.intellij.modules.lang - com.intellij.modules.json - - com.intellij.java - - - org.jetbrains.kotlin - - - - - - - - - - - dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.intention.DataGeneratorIntentionAction - - messages.LangBundle - intention.category.other - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + com.intellij.modules.platform + com.intellij.modules.lang + com.intellij.modules.json + + com.intellij.java + + + org.jetbrains.kotlin + + + + + + + + + + + + + dev.turingcomplete.intellijdevelopertoolsplugin.tool.editor.intention.DataGeneratorIntentionAction + + messages.LangBundle + intention.category.other + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/messages/GeneralBundle.properties b/src/main/resources/messages/GeneralBundle.properties deleted file mode 100644 index e596f66d..00000000 --- a/src/main/resources/messages/GeneralBundle.properties +++ /dev/null @@ -1,19 +0,0 @@ -editor.soft-wrap=Soft-Wrap -editor.show-special-characters=Show Special Characters -editor.show-whitespaces=Show Whitespaces -editor.settings=Settings -editor.mode.input=input -editor.mode.output=output -editor.mode.input-output=input/output -editor.apply=Apply -editor.expand-editor-action-title=Expand Editor -editor.open-file-action-title=Open from File -editor.additional-actions=Additional Actions -editor.clipboard=Clipboard -editor.show-diff-with-clipboard=Show Diff with Clipboard -editor.diff-no-title-fallback=Content -editor.show-diff-with-title=Show Diff with {0} -editor.clear-content-action-title=Clear Content -editor.copy-to-clipboard-action-title=Copy to Clipboard -editor.save-to-file-action-title=Save to File -editor.file-saver-description=Save content as \ No newline at end of file diff --git a/src/main/resources/messages/GeneralBundle_de.properties b/src/main/resources/messages/GeneralBundle_de.properties deleted file mode 100644 index d0d2a4ef..00000000 --- a/src/main/resources/messages/GeneralBundle_de.properties +++ /dev/null @@ -1,19 +0,0 @@ -editor.soft-wrap=Weicher Zeilenumbruch -editor.show-special-characters=Spezielle Zeichen anzeigen -editor.show-whitespaces=Leerzeichen anzeigen -editor.settings=Einstellungen -editor.mode.input=Eingabe -editor.mode.output=Ausgabe -editor.mode.input-output=Eingabe/Ausgabe -editor.apply=Anwenden -editor.expand-editor-action-title=Editor vergr\u00f6\u00dfern -editor.open-file-action-title=Aus Datei \u00f6ffnen -editor.additional-actions=Weitere Aktionen -editor.clipboard=Zwischenablage -editor.show-diff-with-clipboard=Unterschiede mit Zwischenablage anzeigen -editor.diff-no-title-fallback=Inhalt -editor.show-diff-with-title=Unterschiede mit {0} anzeigen -editor.clear-content-action-title=Inhalt leeren -editor.copy-to-clipboard-action-title=In Zwischenablage kopieren -editor.save-to-file-action-title=In Datei speichern -editor.file-saver-description=Inhalt speichern als \ No newline at end of file diff --git a/src/main/resources/messages/UiToolsBundle.properties b/src/main/resources/messages/UiToolsBundle.properties deleted file mode 100644 index 4d3a289d..00000000 --- a/src/main/resources/messages/UiToolsBundle.properties +++ /dev/null @@ -1,51 +0,0 @@ -ascii-art.menu-title=ASCII Art -ascii-art.content-title=ASCII Art -ascii-art.font=Font: -ascii-art.example=Example -ascii-art.examples=Examples -ascii-art.output-title=ASCII Art -ascii-art.download-additional-ascii-art-fonts=Download ASCII Art Fonts From GitHub -ascii-art.download-additional-ascii-art-fonts-help=Download additional ASCII font files from the xero/figlet-fonts GitHub repository.

    Some font files may not work correctly and will be filtered out.

    These additional font files are not part of this plugin and are subject to individual licences. -ascii-art.download-additional-ascii-art-fonts-failed-title=Download Files From GitHub -ascii-art.download-additional-ascii-art-fonts-failed-details=Not all files could be downloaded. See idea.log for more details. -server-certificates.menu-title=Server Certificates -server-certificates.content-title=Server Certificates -server-certificates.url=URL: -server-certificates.follow-redirects=Follow redirects -server-certificates.allow-insecure-connection=Allow insecure connection -server-certificates.allow-insecure-connection-help=Enabling this option permits connections to servers that have invalid server certificates.

    Use this option if you get any SSLHandshakeException errors. -server-certificates.fetch-server-certificates=Fetch Server Certificates -server-certificates.fetch-server-certificates-in-progress=Fetching server certificates... -server-certificates.fetch-server-certificates-in-progress-title=Fetching server certificates -server-certificates.fetch-server-certificates-failed=Failed to retrieve server certificates: {0} -server-certificates.result=Server Certificates -server-certificates.response=Response: {0} {1} -server-certificates.no-result=Connection didn't use any server certificates -server-certificates.certificate-title=Server Certificate {0} -server-certificates.certificate-expired=Certificate is expired -server-certificates.certificate-not-valid-yet=Certificate is not yet valid -server-certificates.certificate-subject=Subject -server-certificates.certificate-issuer=Issuer -server-certificates.certificate-serial-number=Serial Number -server-certificates.certificate-valid-from=Valid From -server-certificates.certificate-valid-to=Valid To -server-certificates.certificate-signature-algorithm=Signature Algo. -server-certificates.export-action-title=Export as {0} -server-certificates.export-failed=Failed to export certificate(s): {0} -server-certificates.export-jks-result=Certificates exported as JKS with password: {0} -server-certificates.copy-pem-to-clipboard-action-title=Copy as PEM to Clipboard -server-certificates.show-as-pem-action-title=Show as PEM -server-certificates.show-certificate-details-action-title=Show Certificate Details -regular-expression-matcher.text-input-title=Text -regular-expression-matcher.menu-title=Regular Expression -regular-expression-matcher.content-title=Regular Expression Matcher -regular-expression-matcher.regex-input=Regular expression: -regular-expression-matcher.replace-pattern-context-help=A matching group can be referenced using a dollar sign followed by the group's index number. For example, $1 refers to the first matching group.

    The group index $0 represents the entire matched text.

    To avoid ambiguity when referencing groups, curly brackets can be used to delimit the index number. For instance, ${1}3 explicitly refers to the group with index 1, followed by the character "3". -regular-expression-matcher.matches-title=Matches -regular-expression-matcher.matches-match-prefix=Match: -regular-expression-matcher.matches-group-prefix=Group: -regular-expression-matcher.matches-no-matches=No matches -regular-expression-matcher.matches-group=Group -regular-expression-matcher.matches-value=Value -regular-expression-matcher.extraction-title=Extraction -regular-expression-matcher.substitution-title=Substitution diff --git a/src/main/resources/messages/UiToolsBundle_de.properties b/src/main/resources/messages/UiToolsBundle_de.properties deleted file mode 100644 index 321decbf..00000000 --- a/src/main/resources/messages/UiToolsBundle_de.properties +++ /dev/null @@ -1,51 +0,0 @@ -ascii-art.menu-title=ASCII-Art -ascii-art.content-title=ASCII-Art -ascii-art.font=Schriftart: -ascii-art.example=Beispiel -ascii-art.examples=Beispiele -ascii-art.output-title=ASCII-Art -ascii-art.download-additional-ascii-art-fonts=ASCII-Art-Schriftarten von GitHub herunterladen -ascii-art.download-additional-ascii-art-fonts-help=Zus\u00e4tzliche ASCII-Schriftartdateien aus dem xero/figlet-fonts-GitHub-Repository herunterladen.

    Einige Schriftartdateien funktionieren m\u00f6glicherweise nicht korrekt und werden herausgefiltert.

    Diese zus\u00e4tzlichen Schriftartdateien sind nicht Teil dieses Plugins und unterliegen individuellen Lizenzen. -ascii-art.download-additional-ascii-art-fonts-failed-title=GitHub-Dateien Herunterladen -ascii-art.download-additional-ascii-art-fonts-failed-details=Nicht alle Dateien konnten heruntergeladen werden. Weitere Details finden Sie in dem idea.log. -server-certificates.menu-title=Server-Zertifikate -server-certificates.content-title=Server-Zertifikate -server-certificates.url=URL: -server-certificates.follow-redirects=Weiterleitungen folgen -server-certificates.allow-insecure-connection=Unsichere Verbindung zulassen -server-certificates.allow-insecure-connection-help=Wenn diese Option aktiviert ist, werden Verbindungen zu Servern mit ung\u00fcltigen Serverzertifikaten zugelassen.

    Verwenden Sie diese Option, wenn SSLHandshakeException-Fehler auftreten. -server-certificates.fetch-server-certificates=Server-Zertifikate abrufen -server-certificates.fetch-server-certificates-in-progress=Server-Zertifikate werden abgerufen... -server-certificates.fetch-server-certificates-in-progress-title=Server-Zertifikate werden abgerufen -server-certificates.fetch-server-certificates-failed=Fehler beim Abrufen der Server-Zertifikate: {0} -server-certificates.result=Server-Zertifikate -server-certificates.response=Antwort: {0} {1} -server-certificates.no-result=Die Verbindung hat keine Server-Zertifikate verwendet -server-certificates.certificate-title=Server Zertifikat {0} -server-certificates.certificate-expired=Zertifikat ist abgelaufen -server-certificates.certificate-not-valid-yet=Zertifikat ist noch nicht g\u00fcltig -server-certificates.certificate-subject=Subject -server-certificates.certificate-issuer=Aussteller -server-certificates.certificate-serial-number=Seriennummer -server-certificates.certificate-valid-from=G\u00fcltig ab -server-certificates.certificate-valid-to=G\u00fcltig bis -server-certificates.certificate-signature-algorithm=Signatur-Algo. -server-certificates.export-action-title=Zertifikat(e) als {0} exportieren -server-certificates.export-failed=Fehler beim Export der Zertifikate: {0} -server-certificates.export-jks-result=Zertifikate wurden als JKS mit dem Passwort {0} exportiert -server-certificates.copy-pem-to-clipboard-action-title=Als PEM in die Zwischenablage kopieren -server-certificates.show-as-pem-action-title=Als PEM anzeigen -server-certificates.show-certificate-details-action-title=Zertifikatsdetails anzeigen -regular-expression-matcher.text-input-title=Text -regular-expression-matcher.menu-title=Regulärer Ausdruck -regular-expression-matcher.content-title=Regulärer Ausdruck Matcher -regular-expression-matcher.matches-title=Treffer -regular-expression-matcher.matches-match-prefix=Treffer: -regular-expression-matcher.matches-group-prefix=Gruppe: -regular-expression-matcher.matches-no-matches=Keine Treffer -regular-expression-matcher.regex-input=Regulärer Ausdruck: -regular-expression-matcher.replace-pattern-context-help=Ein übereinstimmender Group kann mit einem Dollarzeichen, gefolgt von der Indexnummer der Gruppe, referenziert werden. Zum Beispiel bezieht sich $1 auf die erste übereinstimmende Gruppe.

    Der Gruppenindex $0 repräsentiert den gesamten übereinstimmenden Text.

    Um Mehrdeutigkeiten zu vermeiden, können geschweifte Klammern verwendet werden, um die Indexnummer zu begrenzen. Zum Beispiel bezieht sich ${1}3 explizit auf die Gruppe mit Index 1, gefolgt von der Zahl "3". -regular-expression-matcher.matches-group=Gruppe -regular-expression-matcher.matches-value=Wert -regular-expression-matcher.substitution-title=Ersetzung -regular-expression-matcher.extraction-title=Extraktion diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/BundlesTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/BundlesTest.kt deleted file mode 100644 index d38ca642..00000000 --- a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/message/BundlesTest.kt +++ /dev/null @@ -1,206 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.message - -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.vfs.LocalFileSystem -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiManager -import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory -import com.intellij.testFramework.junit5.RunInEdt -import com.intellij.testFramework.junit5.RunMethodInEdt -import com.intellij.testFramework.junit5.TestApplication -import org.assertj.core.api.Assertions.assertThat -import org.jetbrains.kotlin.psi.KtCallExpression -import org.jetbrains.kotlin.psi.KtDotQualifiedExpression -import org.jetbrains.kotlin.psi.KtElement -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtNameReferenceExpression -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.Arguments -import org.junit.jupiter.params.provider.MethodSource -import java.io.File -import java.util.* - -@RunInEdt(allMethods = false) -@TestApplication -class BundlesTest { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val fixture = IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder("test").fixture - private val disposable = Disposer.newDisposable() - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - @BeforeEach - fun beforeEach() { - fixture.setUp() - } - - @AfterEach - @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) - fun afterEach() { - Disposer.dispose(disposable) - fixture.tearDown() - } - - @ParameterizedTest(name="{0}") - @MethodSource("testVectors_allKotlinSourceFiles") - @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) - fun testMessageKeyUsages( - @Suppress("unused") kotlinSourceFileName: String, - kotlinSourceFile: File - ) { - val psiManager = PsiManager.getInstance(fixture.project) - val virtualFile = LocalFileSystem.getInstance().findFileByPath(kotlinSourceFile.absolutePath)!! - val ktFile = ApplicationManager.getApplication().runReadAction { psiManager.findFile(virtualFile) } as KtFile - ktFile.allKtDotQualifiedExpression().forEach { ktDotQualifiedExpression -> - val receiver = ktDotQualifiedExpression.receiverExpression - if (receiver !is KtNameReferenceExpression) { - return@forEach - } - - val className = receiver.getReferencedName() - val callExpression = ktDotQualifiedExpression.selectorExpression as? KtCallExpression - val methodName = callExpression?.calleeExpression?.text - if (methodName != "message") { - return@forEach - } - - val parameters = callExpression.valueArguments - val messageKey = parameters[0].text.replace("\"", "") - println("Check message: $messageKey") - val message = findMessage(className, messageKey) - assertThat(message).describedAs("Message key: $messageKey").isNotNull - assertThat(parameters.size - 1).describedAs("Message parameters of key: $messageKey").isEqualTo(countUniqueParameters(message!!)) - } - } - - @ParameterizedTest(name = "{0}") - @MethodSource("testVectors_allMessagesBundles") - fun checkAllMessagesBundlesContainTheSameKeys( - @Suppress("unused") bundleName: String, - messagesForLanguageKey: Map> - ) { - assertThat(messagesForLanguageKey.keys) - .describedAs("Required language keys") - .containsExactlyInAnyOrderElementsOf(setOf(REFERENCE_LANGUAGE_KEY, "de")) - - val referenceMessages = messagesForLanguageKey[REFERENCE_LANGUAGE_KEY]!! - messagesForLanguageKey.forEach { (languageKey, messages) -> - if (languageKey == REFERENCE_LANGUAGE_KEY) { - return@forEach - } - - // Check same keys - assertThat(referenceMessages.keys) - .describedAs("Same message keys") - .containsExactlyInAnyOrderElementsOf(referenceMessages.keys) - - // Check messages contain the same parameter counts - referenceMessages.keys.forEach { referenceMessageKey -> - assertThat(countUniqueParameters(messages[referenceMessageKey]!!)) - .isEqualTo(countUniqueParameters(referenceMessages[referenceMessageKey]!!)) - } - } - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - - private fun countUniqueParameters(message: String): Int = - Regex("(? { - val methodCalls = mutableListOf() - - fun traverseElement(element: KtElement) { - if (element is KtDotQualifiedExpression) { - methodCalls.add(element) - } - else { - element.children - .filter { it is KtElement } - .forEach { traverseElement(it as KtElement) } - } - } - - traverseElement(this) - - return methodCalls - } - - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private lateinit var messagesBundles: Map>> - - private const val REFERENCE_LANGUAGE_KEY = "en" - - @BeforeAll - @JvmStatic - fun setUp() { - messagesBundles = loadMessageBundles(File("src/main/resources/messages")) - assertThat(messagesBundles.size).isGreaterThan(1) - } - - fun findMessage(bundleName: String, key: String, languageKey: String = REFERENCE_LANGUAGE_KEY): String? = - messagesBundles[bundleName]!![languageKey]!![key] - - @JvmStatic - fun testVectors_allKotlinSourceFiles(): Collection = - getAllFiles(File("src/main/kotlin")) - .filter { it.extension == "kt" } - .map { Arguments.of(it.name, it) } - - @JvmStatic - fun testVectors_allMessagesBundles(): Collection = - messagesBundles.map { Arguments.of(it.key, it.value) } - - private fun loadMessageBundles(messagesBundlesDir: File): Map>> { - check(messagesBundlesDir.isDirectory) - - val result = mutableMapOf>>() - - messagesBundlesDir - .listFiles { file -> file.extension == "properties" } - .forEach { file -> - val filename = file.nameWithoutExtension - val parts = filename.split("_") - - val bundleName = if (parts.size > 1) parts[0] else filename - val languageKey = if (parts.size > 1) parts[1] else REFERENCE_LANGUAGE_KEY - - val properties = Properties().apply { - file.inputStream().use { load(it) } - } - - val messagesBundle = result.getOrPut(bundleName) { mutableMapOf() } - messagesBundle.put(languageKey, properties.stringPropertyNames().associate { it to properties.getProperty(it) }) - } - - return result - } - - private fun getAllFiles(directory: File): List { - check(directory.isDirectory) - - val files = mutableListOf() - - directory.listFiles()?.forEach { file -> - if (file.isDirectory) { - files.addAll(getAllFiles(file)) - } - else { - files.add(file) - } - } - - return files - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsInstanceSettingsTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsInstanceSettingsTest.kt deleted file mode 100644 index be1e7cf1..00000000 --- a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/settings/DeveloperToolsInstanceSettingsTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.settings - -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.util.Disposer -import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory -import com.intellij.testFramework.junit5.RunInEdt -import com.intellij.testFramework.junit5.RunMethodInEdt -import com.intellij.testFramework.junit5.TestApplication -import com.intellij.ui.JBColor -import com.intellij.util.application -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.DeveloperUiToolFactoryEp -import dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiTool -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolFactory -import org.assertj.core.api.Assertions.assertThat -import org.assertj.core.api.Assertions.fail -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import java.math.BigDecimal -import java.time.ZoneId -import java.util.* -import kotlin.random.Random - -@RunInEdt(allMethods = false) -@TestApplication -class DeveloperToolsInstanceSettingsTest { - // -- Properties -------------------------------------------------------------------------------------------------- // - - private val fixture = IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder("test").fixture - private val disposable = Disposable { } - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - @BeforeEach - fun beforeEach() { - fixture.setUp() - } - - @AfterEach - @RunMethodInEdt(writeIntent = RunMethodInEdt.WriteIntentMode.True) - fun afterEach() { - Disposer.dispose(disposable) - fixture.tearDown() - } - - @Test - fun `Check various configurations persistent states`() { - IdeaTestFixtureFactory.getFixtureFactory().createLightFixtureBuilder("dd").fixture - - DeveloperToolsApplicationSettings.instance.also { - it.saveConfigurations = true - it.saveInputs = true - it.saveSensitiveInputs = true - } - - val settings = object : DeveloperToolsInstanceSettings() { - init { - loadState(InstanceState()) - } - } - - val developerUiTools = instantiateAllDeveloperUiTools(settings) - - // Delete all example values - DeveloperToolsApplicationSettings.instance.loadExamples = false - resetAllConfigurations(developerUiTools, settings, false) - - // Expect: No configurations have persisted because there are no property changes - // (The persistent state will only include configuration with modified properties) - assertThat(settings.getState().developerToolsConfigurations).isEmpty() - - // Set the values to the example value, despite `loadExamples` is disabled - modifyAllConfigurationProperties(developerUiTools, settings) { property -> - property.example?.let { exampleProvider -> - property.reference.setWithUncheckedCast(exampleProvider(), null) - } - } - - // Expect: No configurations have persisted because there are no property changes - // (Changes in the `loadExamples` setting are not reflected in the existing - // configurations until the application was restarted. Therefore, during the - // modification check, we will always check if the value is equal to the - // example.) - assertThat(settings.getState().developerToolsConfigurations).isEmpty() - - // Set the values to the default value - modifyAllConfigurationProperties(developerUiTools, settings) { property -> - property.reference.setWithUncheckedCast(property.defaultValue, null) - } - - // Expect: No configurations have persisted because there are no property changes - assertThat(settings.getState().developerToolsConfigurations).isEmpty() - - // Load all example values - DeveloperToolsApplicationSettings.instance.loadExamples = true - resetAllConfigurations(developerUiTools, settings, true) - - // Expect: No configurations have persisted because there are no property changes - assertThat(settings.getState().developerToolsConfigurations).isEmpty() - - // Set the values to a random value - modifyAllConfigurationProperties(developerUiTools, settings) { property -> - val randomValue = if (property.key == "timeZoneId") { - val availableZoneIds = ZoneId.getAvailableZoneIds().toList() - availableZoneIds[Random.nextInt(0, availableZoneIds.size - 1)] - } - else { - when (property.defaultValue) { - is String -> property.defaultValue + "foo" - is Int -> property.defaultValue + 1 - is Long -> property.defaultValue + 1 - is BigDecimal -> property.defaultValue.plus(BigDecimal.ONE) - is Boolean -> !property.defaultValue - is LocaleContainer -> { - val availableLocales = Locale.getAvailableLocales() - LocaleContainer(availableLocales[Random.nextInt(0, availableLocales.size - 1)]) - } - - is Enum<*> -> { - val enumConstants = property.defaultValue::class.java.enumConstants - enumConstants[(property.defaultValue.ordinal + 1) % enumConstants.size] - } - - is JBColor -> JBColor(Random.nextInt(0, 255), Random.nextInt(0, 255)) - else -> throw IllegalStateException("Missing property type mapping for: " + property.defaultValue::class) - } - } - property.reference.setWithUncheckedCast(randomValue, null) - } - - // Expect: All configurations (with properties) have been persisted - assertThat(settings.getState().developerToolsConfigurations) - .hasSize(developerUiTools.filter { - settings.getDeveloperToolConfigurations(it.developerToolFactoryEp.id)[0].properties.isNotEmpty() - }.size) - } - - private fun resetAllConfigurations( - developerUiTools: List>, - settings: DeveloperToolsInstanceSettings, - loadExamples: Boolean - ) { - developerUiTools.forEach { (developerUiToolEp, _, _) -> - settings.getDeveloperToolConfigurations(developerUiToolEp.id).forEach { - ApplicationManager.getApplication().invokeAndWait({ - it.reset(null, loadExamples) - }, ModalityState.any()) - } - } - } - - private fun modifyAllConfigurationProperties( - developerUiTools: List>, - settings: DeveloperToolsInstanceSettings, - modify: (DeveloperToolConfiguration.PropertyContainer) -> Unit - ) { - developerUiTools.forEach { (developerUiToolEp, _) -> - settings.getDeveloperToolConfigurations(developerUiToolEp.id).forEach { - ApplicationManager.getApplication().invokeAndWait({ - it.properties.values.forEach { property -> - modify(property) - } - }, ModalityState.any()) - } - } - } - - private fun instantiateAllDeveloperUiTools(settings: DeveloperToolsInstanceSettings): List> { - val developerUiTools = mutableListOf>() - - DeveloperUiToolFactoryEp.EP_NAME.forEachExtensionSafe { developerToolFactoryEp -> - val developerUiToolFactory: DeveloperUiToolFactory<*> = developerToolFactoryEp.createInstance(application) - val context = DeveloperUiToolContext(developerToolFactoryEp.id, false) - val developerToolConfiguration = settings.createDeveloperToolConfiguration(developerToolFactoryEp.id) - val developerUiTool = developerUiToolFactory - .getDeveloperUiToolCreator(fixture.project, disposable, context) - ?.invoke(developerToolConfiguration) - if (developerUiTool != null) { - developerToolConfiguration.wasConsumedByDeveloperTool = true - ApplicationManager.getApplication().invokeAndWait({ - developerUiTool.createComponent() - developerUiTool.activated() - }, ModalityState.any()) - developerUiTools.add(DeveloperUiToolWrapper( - developerToolFactoryEp = developerToolFactoryEp, - developerUiTool = developerUiTool, - developerToolConfiguration = developerToolConfiguration - )) - } - else { - fail("No instance of tool was created: ${developerToolFactoryEp.id}") - } - } - - return developerUiTools - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - - private data class DeveloperUiToolWrapper( - val developerToolFactoryEp: DeveloperUiToolFactoryEp>, - val developerUiTool: T, - val developerToolConfiguration: DeveloperToolConfiguration - ) - - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - @BeforeAll - @JvmStatic - fun beforeAll() { - System.setProperty("java.awt.headless", false.toString()) - } - - @AfterAll - @JvmStatic - fun afterAll() { - System.setProperty("java.awt.headless", false.toString()) - } - } -} \ No newline at end of file diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CliCommandConverterTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CliCommandConverterTest.kt deleted file mode 100644 index a92773eb..00000000 --- a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/converter/CliCommandConverterTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter - -import com.intellij.openapi.Disposable -import com.intellij.testFramework.junit5.TestApplication -import com.intellij.testFramework.junit5.TestDisposable -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import java.util.* - -@TestApplication -class CliCommandConverterTest { - // -- Properties -------------------------------------------------------------------------------------------------- // - - @TestDisposable - lateinit var disposable: Disposable - - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - @Test - fun `test toTarget()`() { - val cliCommandConverter = CliCommandConverter(DeveloperToolConfiguration("Test", UUID.randomUUID(), emptyMap()), disposable, context, null) - cliCommandConverter.toTarget(" app -foo --baz ---baz -foo-bar \"foo -baz\" '-foo-bar'") - val actual = cliCommandConverter.targetText() - assertThat(actual).isEqualTo(""" -app \ - -foo \ - --baz \ - ---baz \ - -foo-bar "foo -baz" '-foo-bar' - """.trimIndent()) - } - - @Test - fun `test toSource()`() { - val cliCommandConverter = CliCommandConverter(DeveloperToolConfiguration("Test", UUID.randomUUID(), emptyMap()), disposable, context, null) - cliCommandConverter.toSource(""" -app \ - -foo \ - --baz \ - ---baz \ - -foo-bar "foo -baz" '-foo-bar' - """.trimIndent()) - val actual = cliCommandConverter.sourceText() - assertThat(actual).isEqualTo("app -foo --baz ---baz -foo-bar \"foo -baz\" '-foo-bar'") - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - private val context = DeveloperUiToolContext("cli-command-converter", true) - } -} \ No newline at end of file diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/editor/intention/GeneralDependentDescriptionTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/editor/intention/GeneralDependentDescriptionTest.kt deleted file mode 100644 index ecf9f9b0..00000000 --- a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/editor/intention/GeneralDependentDescriptionTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.editor.intention - -import DescriptionTest -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource - -class GeneralDependentDescriptionTest : DescriptionTest() { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exported Methods -------------------------------------------------------------------------------------------- // - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionDescriptionHtmlFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionDescriptionHtmlFileExist(intentionActionSimpleClassName) - } - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionBeforeTemplateFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionBeforeTemplateFileExist(intentionActionSimpleClassName) - } - - @ParameterizedTest - @MethodSource("intentionActionSimpleClassNames") - override fun testIntentionActionAfterTemplateFileExist(intentionActionSimpleClassName: String) { - super.doTestIntentionActionAfterTemplateFileExist(intentionActionSimpleClassName) - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // - - companion object { - - @JvmStatic - fun intentionActionSimpleClassNames() = - intentionActionSimpleClassNames("dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.editor.intention") - } -} \ No newline at end of file diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/LoremIpsumGeneratorTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/LoremIpsumGeneratorTest.kt deleted file mode 100644 index 543b0a16..00000000 --- a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/_internal/tool/ui/generator/LoremIpsumGeneratorTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator - -import com.intellij.testFramework.junit5.TestApplication -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperToolConfiguration -import dev.turingcomplete.intellijdevelopertoolsplugin.main.DeveloperUiToolContext -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import java.util.* - -@TestApplication -class LoremIpsumGeneratorTest { - // -- Properties -------------------------------------------------------------------------------------------------- // - // -- Initialization ---------------------------------------------------------------------------------------------- // - // -- Exposed Methods --------------------------------------------------------------------------------------------- // - - @ParameterizedTest - @CsvSource( - delimiter = '|', - value = [ - "0|", - "1|Lorem.", - "2|Lorem ipsum.", - "3|Lorem ipsum dolor.", - "4|Lorem ipsum dolor sit.", - "5|Lorem ipsum dolor sit amet.", - "6|Lorem ipsum dolor sit amet, consectetur.", - "7|Lorem ipsum dolor sit amet, consectetur adipiscing.", - "8|Lorem ipsum dolor sit amet, consectetur adipiscing elit." - ] - ) - fun `test generation of iconic sentence`(atMostWords: Int, expectedSentence: String?) { - val actualSentence = LoremIpsumGenerator(null, DeveloperUiToolContext("lorem-ipsum-generator", true), DeveloperToolConfiguration("Test", UUID.randomUUID(), emptyMap())) { } - .generateIconicText(atMostWords, true) - assertThat(actualSentence.joinToString(" ")).isEqualTo(expectedSentence ?: "") - } - - // -- Private Methods --------------------------------------------------------------------------------------------- // - // -- Inner Type -------------------------------------------------------------------------------------------------- // - // -- Companion Object -------------------------------------------------------------------------------------------- // -} \ No newline at end of file diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/CliCommandConverterTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/CliCommandConverterTest.kt new file mode 100644 index 00000000..a389a754 --- /dev/null +++ b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/CliCommandConverterTest.kt @@ -0,0 +1,82 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.integrationtest + +import com.intellij.openapi.Disposable +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.TestDisposable +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.converter.CliCommandConverter +import java.util.UUID +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +@TestApplication +class CliCommandConverterTest { + // -- Properties ---------------------------------------------------------- // + + @TestDisposable lateinit var disposable: Disposable + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @Test + fun `test toTarget()`() { + val cliCommandConverter = + CliCommandConverter( + DeveloperToolConfiguration("Test", UUID.randomUUID(), emptyMap()), + disposable, + context, + null, + ) + val actual = + String( + cliCommandConverter.doConvertToTarget( + " app -foo --baz ---baz -foo-bar \"foo -baz\" '-foo-bar'".toByteArray() + ) + ) + assertThat(actual) + .isEqualTo( + """ +app \ + -foo \ + --baz \ + ---baz \ + -foo-bar "foo -baz" '-foo-bar' + """ + .trimIndent() + ) + } + + @Test + fun `test toSource()`() { + val cliCommandConverter = + CliCommandConverter( + DeveloperToolConfiguration("Test", UUID.randomUUID(), emptyMap()), + disposable, + context, + null, + ) + val actual = + cliCommandConverter.doConvertToSource( + """ +app \ + -foo \ + --baz \ + ---baz \ + -foo-bar "foo -baz" '-foo-bar' + """ + .trimIndent() + .toByteArray() + ) + assertThat(String(actual)).isEqualTo("app -foo --baz ---baz -foo-bar \"foo -baz\" '-foo-bar'") + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private val context = DeveloperUiToolContext("cli-command-converter", true) + } +} diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/ColorPickerTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/ColorPickerTest.kt new file mode 100644 index 00000000..f738e14c --- /dev/null +++ b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/ColorPickerTest.kt @@ -0,0 +1,74 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.integrationtest + +import com.intellij.openapi.Disposable +import com.intellij.testFramework.junit5.TestApplication +import com.intellij.testFramework.junit5.TestDisposable +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.other.ColorPicker +import java.awt.Color +import java.util.UUID +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +@TestApplication +class ColorPickerTest { + // -- Properties ---------------------------------------------------------- // + + @TestDisposable lateinit var disposable: Disposable + + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = + [ + "0|0|0|255|rgb(0, 0, 0)|rgba(0, 0, 0, 1)|#000000|#000000FF|hsl(0, 0%, 0%)|hsla(0, 0%, 0%, 1)", + "255|255|255|255|rgb(255, 255, 255)|rgba(255, 255, 255, 1)|#FFFFFF|#FFFFFFFF|hsl(0, 0%, 100%)|hsla(0, 0%, 100%, 1)", + "255|0|0|255|rgb(255, 0, 0)|rgba(255, 0, 0, 1)|#FF0000|#FF0000FF|hsl(0, 100%, 50%)|hsla(0, 100%, 50%, 1)", + "0|255|0|255|rgb(0, 255, 0)|rgba(0, 255, 0, 1)|#00FF00|#00FF00FF|hsl(120, 100%, 50%)|hsla(120, 100%, 50%, 1)", + "0|0|255|255|rgb(0, 0, 255)|rgba(0, 0, 255, 1)|#0000FF|#0000FFFF|hsl(240, 100%, 50%)|hsla(240, 100%, 50%, 1)", + "255|255|0|255|rgb(255, 255, 0)|rgba(255, 255, 0, 1)|#FFFF00|#FFFF00FF|hsl(60, 100%, 50%)|hsla(60, 100%, 50%, 1)", + "0|255|255|255|rgb(0, 255, 255)|rgba(0, 255, 255, 1)|#00FFFF|#00FFFFFF|hsl(180, 100%, 50%)|hsla(180, 100%, 50%, 1)", + "255|0|255|255|rgb(255, 0, 255)|rgba(255, 0, 255, 1)|#FF00FF|#FF00FFFF|hsl(300, 100%, 50%)|hsla(300, 100%, 50%, 1)", + "128|128|128|255|rgb(128, 128, 128)|rgba(128, 128, 128, 1)|#808080|#808080FF|hsl(0, 0%, 50.2%)|hsla(0, 0%, 50.2%, 1)", + "195|211|15|168|rgb(195, 211, 15)|rgba(195, 211, 15, 0.66)|#C3D30F|#C3D30FA8|hsl(64.9, 86.73%, 44.31%)|hsla(64.9, 86.73%, 44.31%, 0.66)", + "0|0|0|0|rgb(0, 0, 0)|rgba(0, 0, 0, 0)|#000000|#00000000|hsl(0, 0%, 0%)|hsla(0, 0%, 0%, 0)", + "255|255|255|0|rgb(255, 255, 255)|rgba(255, 255, 255, 0)|#FFFFFF|#FFFFFF00|hsl(0, 0%, 100%)|hsla(0, 0%, 100%, 0)", + "123|45|67|128|rgb(123, 45, 67)|rgba(123, 45, 67, 0.5)|#7B2D43|#7B2D4380|hsl(343.08, 46.43%, 32.94%)|hsla(343.08, 46.43%, 32.94%, 0.5)", + ], + ) + fun `test RGB conversion`( + r: Int, + g: Int, + b: Int, + a: Int, + expectedRgb: String, + expectedRgba: String, + expectedHex: String, + expectedHexAlpha: String, + expectedHsl: String, + expectedHsla: String, + ) { + val colorPicker = + ColorPicker( + null, + DeveloperToolConfiguration("Test", UUID.randomUUID(), emptyMap()), + disposable, + ) + + val cssValues = colorPicker.createCssValues(Color(r, g, b, a)) + assertThat(cssValues.rgb).isEqualTo(expectedRgb) + assertThat(cssValues.rgbWithAlpha).isEqualTo(expectedRgba) + assertThat(cssValues.hex).isEqualTo(expectedHex) + assertThat(cssValues.hexWithAlpha).isEqualTo(expectedHexAlpha) + assertThat(cssValues.hls).isEqualTo(expectedHsl) + assertThat(cssValues.hlsWithAlpha).isEqualTo(expectedHsla) + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/DeveloperToolsInstanceSettingsTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/DeveloperToolsInstanceSettingsTest.kt new file mode 100644 index 00000000..7021fac9 --- /dev/null +++ b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/DeveloperToolsInstanceSettingsTest.kt @@ -0,0 +1,635 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.integrationtest + +import com.intellij.openapi.util.JDOMUtil +import com.intellij.ui.JBColor +import com.intellij.util.xmlb.XmlSerializer +import dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginInfo +import dev.turingcomplete.intellijdevelopertoolsplugin.common.PluginInfo.PluginVersion.Companion.toPluginVersion +import dev.turingcomplete.intellijdevelopertoolsplugin.common.testfixtures.IdeaTest +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsApplicationSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings.Companion.configurationPropertyTypesByNamesAndLegacyValueTypes +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings.DeveloperToolConfigurationState +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings.InstanceState +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettings.StatePropertyValueConverter +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolsInstanceSettingsLegacy +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.testfixtures.DeveloperUiToolUnderTest +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.testfixtures.DeveloperUiToolsInstances.createDeveloperUiToolsUnderTest +import java.math.BigDecimal +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.nio.file.StandardOpenOption +import java.util.Locale +import java.util.SortedMap +import kotlin.io.path.bufferedReader +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import kotlin.io.path.name +import kotlin.io.path.writeText +import kotlin.io.path.writer +import kotlin.reflect.KClass +import kotlin.streams.asSequence +import org.apache.commons.csv.CSVFormat +import org.apache.commons.csv.CSVParser +import org.apache.commons.csv.CSVPrinter +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.DynamicContainer.dynamicContainer +import org.junit.jupiter.api.DynamicNode +import org.junit.jupiter.api.DynamicTest +import org.junit.jupiter.api.DynamicTest.dynamicTest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestFactory + +class DeveloperToolsInstanceSettingsTest : IdeaTest() { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @TestFactory + fun `Check various configurations persistent states`(): List { + val developerUiToolIdsWithoutPersistentState = setOf("rubber-duck", "intellij-internals") + + val testNodes = mutableListOf() + + DeveloperToolsApplicationSettings.generalSettings.saveConfigurations.set(true) + DeveloperToolsApplicationSettings.generalSettings.saveInputs.set(true) + DeveloperToolsApplicationSettings.generalSettings.saveSensitiveInputs.set(true) + + val settings = createDeveloperToolsInstanceSettings() + + val developerUiToolsUnderTest: List> = + createDeveloperUiToolsUnderTest(fixture.project, disposable, settings) + + testNodes.add( + dynamicTest("No persisted properties if values are empty") { + // Delete all example values + developerUiToolsUnderTest.forEach { it.resetConfiguration(loadExamples = false) } + + // Expect: No configurations have been persisted because there are no property changes + // (The persistent state will only include configuration with modified properties) + assertThat(settings.getState().developerToolsConfigurations).isEmpty() + } + ) + + testNodes.add( + dynamicTest("No persisted properties if they explicitly set to their example value") { + // Set the values to the example value, despite `loadExamples` is disabled + DeveloperToolsApplicationSettings.generalSettings.loadExamples.set(false) + developerUiToolsUnderTest.forEach { + it.modifyAllConfigurationProperties { property -> + property.example?.let { exampleProvider -> exampleProvider() } + } + } + + // Expect: No configurations have persisted because there are no property changes + // (Changes in the `loadExamples` setting are not reflected in the existing + // configurations until the application was restarted. Therefore, during the + // modification check, we will always check if the value is equal to the + // example.) + assertThat(settings.getState().developerToolsConfigurations).isEmpty() + } + ) + + testNodes.add( + dynamicTest("No persisted properties if they explicitly set to their default value") { + // Set the values to the default value + developerUiToolsUnderTest.forEach { + it.modifyAllConfigurationProperties { property -> property.defaultValue } + } + + // Expect: No configurations have persisted because there are no property changes + assertThat(settings.getState().developerToolsConfigurations).isEmpty() + } + ) + + testNodes.add( + dynamicTest("No persisted properties after they have been reset") { + // Load all example values + DeveloperToolsApplicationSettings.Companion.generalSettings.loadExamples.set(true) + developerUiToolsUnderTest.forEach { it.resetConfiguration(loadExamples = true) } + + // Expect: No configurations have persisted because there are no property changes + assertThat(settings.getState().developerToolsConfigurations).isEmpty() + } + ) + + // Set the values to a random value + val randomValuesTestNodes = + developerUiToolsUnderTest + .filter { !developerUiToolIdsWithoutPersistentState.contains(it.id) } + .map { developerUiToolUnderTest -> + developerUiToolUnderTest.randomiseConfiguration() + + val actualPersistedStates: Map = + settings.getState().developerToolsConfigurations!!.associateBy { it.developerToolId!! } + + dynamicContainer( + developerUiToolUnderTest.id, + listOf( + dynamicTest("Developer tool has a persisted state") { + assertThat(actualPersistedStates).containsKey(developerUiToolUnderTest.id) + }, + dynamicTest("All properties have an persisted state") { + val actualDeveloperToolConfigurationPropertiesKeys = + actualPersistedStates[developerUiToolUnderTest.id]!!.properties!!.map { property + -> + property.key!! + } + assertThat(actualDeveloperToolConfigurationPropertiesKeys) + .containsExactlyInAnyOrderElementsOf( + developerUiToolUnderTest.configuration.properties.keys + ) + }, + ), + ) + } + testNodes.add(dynamicContainer("Persisted state", randomValuesTestNodes)) + + return testNodes + } + + @TestFactory + fun `Test fromPersistent and toPersistent of built-in property types`(): List { + val testVectors: Map, Pair> = + mapOf( + Boolean::class to Pair(true, "true"), + Int::class to Pair(42, "42"), + Long::class to Pair(-84L, "-84"), + Double::class to Pair(1234567.0, "1234567.0"), + Float::class to Pair(1.2345f, "1.2345"), + String::class to Pair("foo", "foo"), + JBColor::class to Pair(JBColor.MAGENTA, "-65281"), + LocaleContainer::class to Pair(LocaleContainer(Locale.forLanguageTag("de-DE")), "de-DE"), + BigDecimal::class to + Pair(BigDecimal(1.234), "1.2339999999999999857891452847979962825775146484375"), + ) + + return DeveloperToolsInstanceSettings.builtInConfigurationPropertyTypes.map { + (type, propertyType) -> + dynamicTest(type.qualifiedName!!) { + assertThat(testVectors).containsKey(type) + val (inputValue, persistedValue) = testVectors[type]!! + assertThat(propertyType.toPersistent(inputValue)).isEqualTo(persistedValue) + assertThat(propertyType.fromPersistent(persistedValue)).isEqualTo(inputValue) + } + } + } + + @Test + fun `Legacy settings import test data for current plugin version exists`() { + val legacyImportDirForPluginVersion = + instanceSettingsResourcesDir.resolve(PluginInfo.pluginVersion.toString()) + assertThat(legacyImportDirForPluginVersion).exists() + } + + @Test + @Disabled + fun `Create settings import test data for current plugin version`() { + val legacyImportDirForPluginVersion = + instanceSettingsResourcesDir.resolve(PluginInfo.pluginVersion.toString()) + if (Files.notExists(legacyImportDirForPluginVersion)) { + legacyImportDirForPluginVersion.createDirectories() + } + + DeveloperToolsApplicationSettings.generalSettings.saveConfigurations.set(true) + DeveloperToolsApplicationSettings.generalSettings.saveInputs.set(true) + DeveloperToolsApplicationSettings.generalSettings.saveSensitiveInputs.set(true) + + val settings = createDeveloperToolsInstanceSettings() + + val developerUiToolsUnderTest: List> = + createDeveloperUiToolsUnderTest(fixture.project, disposable, settings) + + developerUiToolsUnderTest.forEach { it.randomiseConfiguration() } + + legacyImportDirForPluginVersion + .resolve(EXPECTED_CONFIGURATION_PROPERTIES_CSV_FILENAME) + .writer(options = arrayOf(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) + .use { writer -> + CSVPrinter( + writer, + expectedConfigurationPropertiesCsvFormat.builder().setSkipHeaderRecord(false).build(), + ) + .use { printer -> + settings + .getState() + .developerToolsConfigurations!! + .sortedBy { it.developerToolId } + .forEach { + it.properties!! + .sortedBy { it.key } + .forEach { property -> + val persistedValue = + StatePropertyValueConverter().toString(property.value!!).split("|", limit = 2) + printer.printRecord( + it.developerToolId, + property.key!!, + property.type!!.name, + persistedValue[0], + persistedValue[1], + ) + } + } + } + } + + legacyImportDirForPluginVersion + .resolve(INSTANCE_SETTINGS_PERSISTED_STATE_XML_FILENAME) + .writer(options = arrayOf(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) + .use { writer -> + val element = XmlSerializer.serialize(settings.getState()) + writer.write(JDOMUtil.write(element)) + } + } + + @Test + @Disabled + fun `Collect changed configuration properties of current plugin version`() { + val pluginVersion = + walkInstanceSettingsResourcesDir().map { it.name.toPluginVersion() }.sorted().toList() + val previousPluginVersion = pluginVersion[pluginVersion.lastIndex - 1] + assertThat(previousPluginVersion).isLessThan(PluginInfo.pluginVersion) + + val instanceSettingsVersionDir = + walkInstanceSettingsResourcesDir() + .filter { it.name == previousPluginVersion.toString() } + .first() + + val persistedStateXmlFile = + instanceSettingsVersionDir.resolve(INSTANCE_SETTINGS_PERSISTED_STATE_XML_FILENAME) + val expectedConfigurationPropertiesCsvFile = + instanceSettingsVersionDir.resolve(EXPECTED_CONFIGURATION_PROPERTIES_CSV_FILENAME) + + DeveloperToolsApplicationSettings.generalSettings.saveConfigurations.set(true) + DeveloperToolsApplicationSettings.generalSettings.saveInputs.set(true) + DeveloperToolsApplicationSettings.generalSettings.saveSensitiveInputs.set(true) + + val restoredSettings = + createDeveloperToolsInstanceSettings( + instantState = + XmlSerializer.deserialize( + persistedStateXmlFile.toUri().toURL(), + InstanceState::class.java, + ) + ) + + val developerUiToolsUnderTest: Map> = + createDeveloperUiToolsUnderTest(fixture.project, disposable, restoredSettings).associateBy { + it.id + } + + data class RenamedConfigurationProperty( + val developerToolId: String, + val propertyKey: String, + val propertyKeyAfterLegacies: String, + ) + data class RemovedConfigurationProperty(val developerToolId: String, val propertyKey: String) + + val renamedConfigurationProperties = mutableListOf() + val removedConfigurationProperties = mutableListOf() + + readExpectedConfigurationProperties(expectedConfigurationPropertiesCsvFile).forEach { + (developerToolId, propertyKey, _, _, _) -> + // If `actualConfiguration` is null, the whole developer tool was removed + val actualConfiguration = developerUiToolsUnderTest[developerToolId]?.configuration + + if (actualConfiguration?.properties?.containsKey(propertyKey) == true) { + return@forEach + } + + val propertyKeyAfterLegacies = + DeveloperToolsInstanceSettingsLegacy.applyConfigurationPropertyKeyLegacies( + null, + developerToolId, + propertyKey, + ) + + if ( + propertyKeyAfterLegacies != propertyKey && + actualConfiguration != null && + actualConfiguration.properties.containsKey(propertyKeyAfterLegacies) + ) { + renamedConfigurationProperties.add( + RenamedConfigurationProperty( + developerToolId = developerToolId, + propertyKey = propertyKey, + propertyKeyAfterLegacies = propertyKeyAfterLegacies, + ) + ) + } else { + removedConfigurationProperties.add( + RemovedConfigurationProperty(developerToolId = developerToolId, propertyKey = propertyKey) + ) + } + } + + fun writePropertiesFile( + filename: String, + csvFormat: CSVFormat, + writeProperties: CSVPrinter.() -> Unit, + ) { + instanceSettingsResourcesDir + .resolve(PluginInfo.pluginVersion.toString()) + .resolve(filename) + .apply { writeText("") } + .writer(options = arrayOf(StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) + .use { writer -> + val csvPrinter = + CSVPrinter(writer, csvFormat.builder().setSkipHeaderRecord(false).build()) + writeProperties(csvPrinter) + } + } + + writePropertiesFile( + RENAMED_CONFIGURATION_PROPERTIES_CSV_FILENAME, + renamedConfigurationPropertiesCsvFormat, + ) { + renamedConfigurationProperties.forEach { + printRecord(it.developerToolId, it.propertyKey, it.propertyKeyAfterLegacies) + } + } + + writePropertiesFile( + REMOVED_CONFIGURATION_PROPERTIES_CSV_FILENAME, + removedConfigurationPropertiesCsvFormat, + ) { + removedConfigurationProperties.forEach { printRecord(it.developerToolId, it.propertyKey) } + } + } + + @TestFactory + fun `Test legacy settings import`(): List { + val renamedProperties = collectRenamedConfigurationProperties() + val removedProperties = collectRemovedConfigurationProperties() + + val checkInstanceSettings: (Path) -> DynamicContainer = { instanceSettingsVersionDir -> + val persistedStateXmlFile = + instanceSettingsVersionDir.resolve(INSTANCE_SETTINGS_PERSISTED_STATE_XML_FILENAME) + val expectedConfigurationPropertiesCsvFile = + instanceSettingsVersionDir.resolve(EXPECTED_CONFIGURATION_PROPERTIES_CSV_FILENAME) + + DeveloperToolsApplicationSettings.generalSettings.saveConfigurations.set(true) + DeveloperToolsApplicationSettings.generalSettings.saveInputs.set(true) + DeveloperToolsApplicationSettings.generalSettings.saveSensitiveInputs.set(true) + + val restoredSettings = + createDeveloperToolsInstanceSettings( + instantState = + XmlSerializer.deserialize( + persistedStateXmlFile.toUri().toURL(), + InstanceState::class.java, + ) + ) + + val developerUiToolsUnderTest: Map> = + createDeveloperUiToolsUnderTest(fixture.project, disposable, restoredSettings).associateBy { + it.id + } + + val expectedProperties = + readExpectedConfigurationProperties(expectedConfigurationPropertiesCsvFile) + + val checkRestoredProperty: (ExpectedConfigurationProperty) -> DynamicTest = + { + ( + developerToolId, + expectedPropertyKey, + expectedPropertyValueTypeName, + expectedPropertyValue, + expectedPropertyType) -> + dynamicTest(expectedPropertyKey) { + val actualConfiguration = developerUiToolsUnderTest[developerToolId]?.configuration + + assertThat(actualConfiguration).isNotNull + + // Property was loaded from the persisted state + val expectedPropertyKeyAfterRename = + applyRenamedConfigurationPropertyKeys( + renamedConfigurationProperties = renamedProperties, + developerToolId = developerToolId, + propertyKey = expectedPropertyKey, + ) + assertThat(actualConfiguration!!.persistentProperties) + .containsKey(expectedPropertyKeyAfterRename) + + // Property was restored from persisted state during property registration + assertThat(actualConfiguration.properties).containsKey(expectedPropertyKeyAfterRename) + val actualProperty = actualConfiguration.properties[expectedPropertyKeyAfterRename] + + // Property type is allowed + assertThat(configurationPropertyTypesByNamesAndLegacyValueTypes) + .containsKey(expectedPropertyValueTypeName) + + val propertyValueType = + configurationPropertyTypesByNamesAndLegacyValueTypes[expectedPropertyValueTypeName] + + val expectedValue = propertyValueType!!.fromPersistent(expectedPropertyValue) + // Property value was correctly read from its persisted state + assertThat( + actualConfiguration.persistentProperties[expectedPropertyKeyAfterRename]!!.value + ) + .isEqualTo(expectedValue) + // Property value was correctly restored from its persisted state + // This test may in some cases not really test the restore, because the + // value was overwritten during the tool initialization (e.g., source + // to target text transformation). + assertThat(actualProperty!!.reference.get()).isEqualTo(expectedValue) + + // Property type was correctly restored + assertThat(actualProperty.type.name).isEqualTo(expectedPropertyType) + } + } + + dynamicContainer( + instanceSettingsVersionDir.fileName.toString(), + expectedProperties + .filter { expectedProperty -> + removedProperties.values.none { + it[expectedProperty.developerToolId]?.contains(expectedProperty.propertyKey) == true + } + } + .groupBy { it.developerToolId } + .map { expectedProperty -> + dynamicContainer( + expectedProperty.key, + expectedProperty.value.map(checkRestoredProperty), + ) + }, + ) + } + + return walkInstanceSettingsResourcesDir().map(checkInstanceSettings).toList() + } + + private fun readExpectedConfigurationProperties( + expectedConfigurationPropertiesCsvFile: Path + ): List = + expectedConfigurationPropertiesCsvFile.bufferedReader().use { reader -> + CSVParser(reader, expectedConfigurationPropertiesCsvFormat).records.map { record -> + val developerToolId = record[CSV_HEADER_DEVELOPER_TOOL_ID] + ExpectedConfigurationProperty( + developerToolId = developerToolId, + propertyKey = record[CSV_HEADER_PROPERTY_KEY], + propertyValueType = record[CSV_HEADER_PROPERTY_VALUE_TYPE_NAME], + propertyValue = record[CSV_HEADER_PROPERTY_VALUE].replace("\r\n", System.lineSeparator()), + propertyType = record[CSV_HEADER_PROPERTY_TYPE], + ) + } + } + + private fun applyRenamedConfigurationPropertyKeys( + renamedConfigurationProperties: + SortedMap>>, + developerToolId: String, + propertyKey: String, + ): String { + var newPropertyName = propertyKey + for ((_, renamedProperties) in renamedConfigurationProperties) { + if (renamedProperties.containsKey(developerToolId)) { + val renamedPropertiesForDeveloperTool = renamedProperties[developerToolId] + if (renamedPropertiesForDeveloperTool?.containsKey(propertyKey) == true) { + newPropertyName = renamedPropertiesForDeveloperTool[propertyKey]!! + } + } + } + return newPropertyName + } + + // -- Private Methods ----------------------------------------------------- // + + private fun createDeveloperToolsInstanceSettings( + instantState: InstanceState = InstanceState() + ): DeveloperToolsInstanceSettings = + object : DeveloperToolsInstanceSettings() { + + init { + loadState(instantState) + } + } + + private fun collectRenamedConfigurationProperties(): + SortedMap>> = + walkInstanceSettingsResourcesDir() + .mapNotNull { dir -> + val csvFile = dir.resolve(RENAMED_CONFIGURATION_PROPERTIES_CSV_FILENAME) + if (!csvFile.exists()) return@mapNotNull null + + val properties = + csvFile.bufferedReader().use { reader -> + CSVParser(reader, renamedConfigurationPropertiesCsvFormat).records.map { + Triple( + it.get(CSV_HEADER_DEVELOPER_TOOL_ID), + it.get(CSV_HEADER_OLD_PROPERTY_KEY), + it.get(CSV_HEADER_NEW_PROPERTY_KEY), + ) + } + } + + dir.name.toPluginVersion() to properties + } + .associate { (pluginVersion, props) -> + pluginVersion to + props + .groupBy { it.first } + .mapValues { (_, group) -> group.associate { it.second to it.third } } + } + .toSortedMap() + + private fun collectRemovedConfigurationProperties(): + SortedMap>> = + Files.walk(instanceSettingsResourcesDir, 1) + .asSequence() + .filter { it != instanceSettingsResourcesDir && it.isDirectory() } + .mapNotNull { dir -> + val csvFile = dir.resolve(REMOVED_CONFIGURATION_PROPERTIES_CSV_FILENAME) + if (!csvFile.exists()) return@mapNotNull null + + val properties = + csvFile.bufferedReader().use { reader -> + CSVParser(reader, removedConfigurationPropertiesCsvFormat).records.groupBy({ + it.get(CSV_HEADER_DEVELOPER_TOOL_ID) + }) { + it.get(CSV_HEADER_PROPERTY_KEY) + } + } + + dir.name.toPluginVersion() to properties + } + .toMap() + .toSortedMap() + + private fun walkInstanceSettingsResourcesDir(): Sequence = + Files.walk(instanceSettingsResourcesDir, 1) + .filter { it != instanceSettingsResourcesDir } + .filter { it.isDirectory() } + .asSequence() + + // -- Inner Type ---------------------------------------------------------- // + + private data class ExpectedConfigurationProperty( + val developerToolId: String, + val propertyKey: String, + val propertyValueType: String, + val propertyValue: String, + val propertyType: String, + ) + + // -- Companion Object ---------------------------------------------------- // + + companion object { + + private const val EXPECTED_CONFIGURATION_PROPERTIES_CSV_FILENAME = + "expected-configuration-properties.csv" + private const val RENAMED_CONFIGURATION_PROPERTIES_CSV_FILENAME = + "renamed-configuration-properties.csv" + private const val REMOVED_CONFIGURATION_PROPERTIES_CSV_FILENAME = + "removed-configuration-properties.csv" + private const val INSTANCE_SETTINGS_PERSISTED_STATE_XML_FILENAME = + "instance-settings-persisted-state.xml" + private val instanceSettingsResourcesDir = + Paths.get( + "src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings" + ) + + private const val CSV_HEADER_DEVELOPER_TOOL_ID = "developerToolId" + private const val CSV_HEADER_PROPERTY_KEY = "propertyKey" + private const val CSV_HEADER_OLD_PROPERTY_KEY = "oldPropertyKey" + private const val CSV_HEADER_NEW_PROPERTY_KEY = "newPropertyKey" + private const val CSV_HEADER_PROPERTY_TYPE = "propertyType" + private const val CSV_HEADER_PROPERTY_VALUE_TYPE_NAME = "propertyValueTypeName" + private const val CSV_HEADER_PROPERTY_VALUE = "propertyValue" + + private val expectedConfigurationPropertiesCsvFormat = + CSVFormat.Builder.create() + .setHeader( + CSV_HEADER_DEVELOPER_TOOL_ID, + CSV_HEADER_PROPERTY_KEY, + CSV_HEADER_PROPERTY_TYPE, + CSV_HEADER_PROPERTY_VALUE_TYPE_NAME, + CSV_HEADER_PROPERTY_VALUE, + ) + .setSkipHeaderRecord(true) + .build() + + private val renamedConfigurationPropertiesCsvFormat = + CSVFormat.Builder.create() + .setHeader( + CSV_HEADER_DEVELOPER_TOOL_ID, + CSV_HEADER_OLD_PROPERTY_KEY, + CSV_HEADER_NEW_PROPERTY_KEY, + ) + .setSkipHeaderRecord(true) + .build() + + private val removedConfigurationPropertiesCsvFormat = + CSVFormat.Builder.create() + .setHeader(CSV_HEADER_DEVELOPER_TOOL_ID, CSV_HEADER_PROPERTY_KEY) + .setSkipHeaderRecord(true) + .build() + } +} diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/LoremIpsumGeneratorTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/LoremIpsumGeneratorTest.kt new file mode 100644 index 00000000..06f7f9b4 --- /dev/null +++ b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/LoremIpsumGeneratorTest.kt @@ -0,0 +1,48 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.integrationtest + +import com.intellij.testFramework.junit5.TestApplication +import dev.turingcomplete.intellijdevelopertoolsplugin.settings.DeveloperToolConfiguration +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.base.DeveloperUiToolContext +import dev.turingcomplete.intellijdevelopertoolsplugin.tool.ui.generator.LoremIpsumGenerator +import java.util.UUID +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.CsvSource + +@TestApplication +class LoremIpsumGeneratorTest { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exposed Methods ----------------------------------------------------- // + + @ParameterizedTest + @CsvSource( + delimiter = '|', + value = + [ + "0|", + "1|Lorem.", + "2|Lorem ipsum.", + "3|Lorem ipsum dolor.", + "4|Lorem ipsum dolor sit.", + "5|Lorem ipsum dolor sit amet.", + "6|Lorem ipsum dolor sit amet, consectetur.", + "7|Lorem ipsum dolor sit amet, consectetur adipiscing.", + "8|Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + ], + ) + fun `test generation of iconic sentence`(atMostWords: Int, expectedSentence: String?) { + val actualSentence = + LoremIpsumGenerator( + null, + DeveloperUiToolContext("lorem-ipsum-generator", true), + DeveloperToolConfiguration("Test", UUID.randomUUID(), emptyMap()), + ) {} + .generateIconicText(atMostWords, true) + assertThat(actualSentence.joinToString(" ")).isEqualTo(expectedSentence ?: "") + } + + // -- Private Methods ----------------------------------------------------- // + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // +} diff --git a/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/plugin/PluginXmlTest.kt b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/plugin/PluginXmlTest.kt new file mode 100644 index 00000000..7c09125c --- /dev/null +++ b/src/test/kotlin/dev/turingcomplete/intellijdevelopertoolsplugin/plugin/PluginXmlTest.kt @@ -0,0 +1,109 @@ +package dev.turingcomplete.intellijdevelopertoolsplugin.plugin + +import com.intellij.openapi.util.JDOMUtil +import org.assertj.core.api.Assertions.assertThat +import org.jdom.Element +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test + +class PluginXmlTest { + // -- Properties ---------------------------------------------------------- // + // -- Initialization ------------------------------------------------------ // + // -- Exported Methods ---------------------------------------------------- // + + @Test + fun `test that all referenced class name exist`() { + val referencedClassNamesInAttributes = + pluginXml.getAttributeValuesRecursively( + "implementation", + "class", + "implementationClass", + "implements", + "beanClass", + "instance", + "factoryClass", + ) + val referencedClassNamesInValues = + pluginXml.getElementValuesRecursively("className").map { it.trim() } + val referencedClassNames = + referencedClassNamesInAttributes.plus(referencedClassNamesInValues).map { + it.replace("&", ".") + } + assertThat(referencedClassNames).hasSizeGreaterThan(25) + + val missingReferencedClassNames = + referencedClassNames.filter { + try { + println("Loading class: $it") + Class.forName(it) + return@filter false + } catch (_: Exception) { + return@filter true + } + } + + assertThat(missingReferencedClassNames) + .describedAs("No missing referenced class names") + .isEmpty() + } + + @Test + fun `test that all referenced files exist`() { + val referencedFiles = + pluginXml.getAttributeValuesRecursively("config-file", "icon").map { + if (it.startsWith("/")) it else "/META-INF/$it" + } + assertThat(referencedFiles).hasSizeGreaterThan(2) + + val missingReferencedFiles = + referencedFiles.filter { + println("Loading file: $it") + this::class.java.getResource(it) == null + } + + assertThat(missingReferencedFiles).describedAs("No missing referenced files").isEmpty() + } + + // -- Private Methods ----------------------------------------------------- // + + fun Element.getAttributeValuesRecursively(vararg attributeNames: String): List { + val values = mutableListOf() + + attributeNames.forEach { attr -> this.getAttributeValue(attr)?.let { values.add(it) } } + + this.children.forEach { child -> + values.addAll(child.getAttributeValuesRecursively(*attributeNames)) + } + + return values + } + + fun Element.getElementValuesRecursively(vararg elementNames: String): List { + val values = mutableListOf() + + if (this.name in elementNames) { + this.value?.let { values.add(it) } + } + + this.children.forEach { child -> + values.addAll(child.getElementValuesRecursively(*elementNames)) + } + + return values + } + + // -- Inner Type ---------------------------------------------------------- // + // -- Companion Object ---------------------------------------------------- // + + companion object { + + lateinit var pluginXml: Element + + @BeforeAll + @JvmStatic + fun beforeAll() { + pluginXml = + JDOMUtil.load(PluginXmlTest::class.java.getResourceAsStream("/META-INF/plugin.xml")) + } + } +} diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/6.3.0/expected-configuration-properties.csv b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/6.3.0/expected-configuration-properties.csv new file mode 100644 index 00000000..b45989a5 --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/6.3.0/expected-configuration-properties.csv @@ -0,0 +1,451 @@ +developerToolId,propertyKey,propertyType,propertyValueTypeName,propertyValue +text-statistic,text-statistic-text-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-statistic,text-statistic-text-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-statistic,text,INPUT,kotlin.String,6D0itFvr3Cqe6SKJlnzl +text-statistic,text-statistic-text-softWraps,CONFIGURATION,kotlin.Boolean,false +notes,test,INPUT,kotlin.String,B11Rmn3Q78f7ZFaCWe0f +notes,notes-content-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +notes,notes-content-softWraps,CONFIGURATION,kotlin.Boolean,false +notes,notes-content-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +password-generator,addDigits,CONFIGURATION,kotlin.Boolean,false +password-generator,password-generator-bulk-generation-softWraps,CONFIGURATION,kotlin.Boolean,false +password-generator,lettersMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.PasswordGenerator$LettersMode,ASCII_ALPHABET_ONLY_LOWERCASE +password-generator,length,CONFIGURATION,kotlin.Int,31 +password-generator,addSymbols,CONFIGURATION,kotlin.Boolean,false +password-generator,password-generator-bulk-generation-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +password-generator,symbols,CONFIGURATION,kotlin.String,aPEBHbSgZW0GoAtIv5Wl +password-generator,password-generator-bulk-generation-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-diff,firstText,INPUT,kotlin.String,EuNrzB3W1OU22zAbznhc +text-diff,secondText,INPUT,kotlin.String,9yyVdNePePpszblHKH1Q +json-schema-validator,liveValidation,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,json-schema-validator-schema-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,json-schema-validator-data-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,json-schema-validator-data-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,json-schema-validator-schema-softWraps,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,json-schema-validator-data-softWraps,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,dataText,INPUT,kotlin.String,paxRKrKLsIMsZKhJ4jtb +json-schema-validator,schemaText,INPUT,kotlin.String,6Gkhgbbtl2LualBOWIQF +json-schema-validator,json-schema-validator-schema-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +date-time-converter,formattedLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer,vun-TZ +date-time-converter,formattedIndividual,CONFIGURATION,kotlin.Boolean,true +date-time-converter,formattedIndividualFormat,CONFIGURATION,kotlin.String,UPk5C0LsZi6m6uP04tb9 +date-time-converter,timeZoneId,CONFIGURATION,kotlin.String,Africa/Lagos +date-time-converter,formattedStandardFormatAddTimeZone,CONFIGURATION,kotlin.Boolean,true +date-time-converter,formattedStandardFormat,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.DatetimeConverter$StandardFormat,ISO_8601_DATE +date-time-converter,formattedStandardFormatAddOffset,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,base64-encoder-decoder-source-softWraps,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,base64-encoder-decoder-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,base64-encoder-decoder-target-softWraps,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,base64-encoder-decoder-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,base64-encoder-decoder-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,sourceText,INPUT,kotlin.String,ci9aCNj2wB7Hvge1svW1 +base64-encoder-decoder,base64-encoder-decoder-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-text-escape,json-text-escape-source-softWraps,CONFIGURATION,kotlin.Boolean,false +json-text-escape,json-text-escape-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +json-text-escape,json-text-escape-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-text-escape,sourceText,INPUT,kotlin.String,DFxiKKGPVYSkBdtd6OGB +json-text-escape,json-text-escape-target-softWraps,CONFIGURATION,kotlin.Boolean,false +json-text-escape,json-text-escape-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-text-escape,json-text-escape-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-art,textInput,INPUT,kotlin.String,cSUmHF7fddjSDCA705cW +ascii-art,ascii-art-asciiArtOutput-softWraps,CONFIGURATION,kotlin.Boolean,true +ascii-art,selectedFontFileName,CONFIGURATION,kotlin.String,slant.flf +ascii-art,ascii-art-asciiArtOutput-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-art,ascii-art-asciiArtOutput-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,url-encoding-encoder-decoder-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +url-encoding-encoder-decoder,url-encoding-encoder-decoder-source-softWraps,CONFIGURATION,kotlin.Boolean,false +url-encoding-encoder-decoder,sourceText,INPUT,kotlin.String,SfP38R5X92opA4rl1zfQ +url-encoding-encoder-decoder,url-encoding-encoder-decoder-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,url-encoding-encoder-decoder-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,url-encoding-encoder-decoder-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,url-encoding-encoder-decoder-target-softWraps,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,cli-command-converter-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,liveConversion,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,sourceText,INPUT,kotlin.String,2Cf5Wvj5qvtdLtkYnG7u +cli-command-converter,cli-command-converter-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,cli-command-converter-source-softWraps,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,cli-command-converter-target-softWraps,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,lineBreakDelimiter,CONFIGURATION,kotlin.String,18ej9NK7y2Pv82Tb6PXH +cli-command-converter,cli-command-converter-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,targetText,INPUT,kotlin.String,2Cf5Wvj5qvtdLtkYnG7u +cli-command-converter,cli-command-converter-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,ascii-encoder-decoder-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,ascii-encoder-decoder-target-softWraps,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,sourceText,INPUT,kotlin.String,Jr2xQxsx6MYV1ZGG33OH +ascii-encoder-decoder,ascii-encoder-decoder-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,ascii-encoder-decoder-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,targetText,INPUT,kotlin.String,Jr2xQxsx6MYV1ZGG33OH +ascii-encoder-decoder,ascii-encoder-decoder-source-softWraps,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,ascii-encoder-decoder-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,html-entities-escape-target-softWraps,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,html-entities-escape-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,html-entities-escape-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,sourceText,INPUT,kotlin.String,NzZlOxCShjMASRcMXYHP +html-entities-escape,html-entities-escape-source-softWraps,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,html-entities-escape-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,html-entities-escape-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +java-text-escape,java-text-escape-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +java-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +java-text-escape,java-text-escape-source-softWraps,CONFIGURATION,kotlin.Boolean,false +java-text-escape,java-text-escape-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +java-text-escape,sourceText,INPUT,kotlin.String,3lesPTH061rHXQOq29zx +java-text-escape,java-text-escape-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +java-text-escape,java-text-escape-target-softWraps,CONFIGURATION,kotlin.Boolean,false +java-text-escape,java-text-escape-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-encoded-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-header-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-payload-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,headerText,INPUT,kotlin.String,"{ + ""alg"": ""HS512"", + ""typ"": ""JWT"" +}" +jwt-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,secretKeyEncodingMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder$SecretKeyEncodingMode,BASE32 +jwt-encoder-decoder,payloadText,INPUT,kotlin.String,"{ + ""sub"": ""1234567890"", + ""name"": ""John Doe"", + ""admin"": true, + ""iat"": 1516239022 +}" +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-payload-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-header-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,secret,SENSITIVE,kotlin.String,RA6qUMP8UDr6rdtJGBdV +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-encoded-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-payload-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,encodedText,INPUT,kotlin.String,eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.ANCf_8p1AE4ZQs7QuqGAyyfTEgYrKSjKWkhBk5cIn1_2QVr2jEjmM-1tu7EgnyOf_fAsvdFXva8Sv05iTGzETg +jwt-encoder-decoder,privateKey,SENSITIVE,kotlin.String,2Dxv9dHcoSso9QnYhsDb +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-encoded-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,signingKeyValidation,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-header-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,algorithm,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.JwtEncoderDecoder$SignatureAlgorithm,HMAC512 +hmac-transformer,hmac-transformer-source-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,hmac-transformer-result-output-softWraps,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,secretKey,SENSITIVE,kotlin.String,hlyfPEa7BW40mDdY9TE1 +hmac-transformer,hmac-transformer-source-input-softWraps,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,hmac-transformer-result-output-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,liveTransformation,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,secretKeyEncodingMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.HmacTransformer$SecretKeyEncodingMode,BASE32 +hmac-transformer,hmac-transformer-source-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,sourceText,INPUT,kotlin.String,l3gOuOfCgJlLSB3p9czR +hmac-transformer,hmac-transformer-result-output-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-path,json-path-source-input-softWraps,CONFIGURATION,kotlin.Boolean,false +json-path,formatResult,CONFIGURATION,kotlin.Boolean,false +json-path,json-path-result-output-softWraps,CONFIGURATION,kotlin.Boolean,false +json-path,liveTransformation,CONFIGURATION,kotlin.Boolean,false +json-path,json-path-result-output-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-path,sourceText,INPUT,kotlin.String,GEtQmilgk3Vtc34Hm36b +json-path,contentText,INPUT,kotlin.String,xHhhXu7lRLx7iqfAxOhB +json-path,json-path-source-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-path,json-path-source-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-path,json-path-result-output-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,code-style-formatting-source-input-softWraps,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,liveTransformation,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,code-style-formatting-result-output-softWraps,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,code-style-formatting-result-output-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,sourceText,INPUT,kotlin.String, +code-style-formatting,languageId,CONFIGURATION,kotlin.String,XML +code-style-formatting,code-style-formatting-result-output-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,code-style-formatting-source-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,code-style-formatting-source-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +units-converter,lastSelectedUnitConverterIndex,CONFIGURATION,kotlin.Int,1 +units-converter,dataSizeConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,dataSizeConverter_bitDataSizeValue,INPUT,java.math.BigDecimal,1 +units-converter,baseConverter_baseTwoInput,INPUT,kotlin.String,0111110 +units-converter,transferRateConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer,it-SM +units-converter,baseConverter_showOnlyCommonBases,CONFIGURATION,kotlin.Boolean,false +units-converter,timeConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,dataSizeConverter_roundingMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.MathContextUnitConverter$RoundingMode,HALF_DOWN +units-converter,transferRateConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,transferRateConverter_showLargeDataUnits,CONFIGURATION,kotlin.Boolean,true +units-converter,timeConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer,ff-Latn-GM +units-converter,dataSizeConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +units-converter,timeConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +units-converter,transferRateConverter_timeDimension,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.TransferRateConverter$TransferRateTimeDimension,MINUTES +units-converter,timeConverter_roundingMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.MathContextUnitConverter$RoundingMode,HALF_DOWN +units-converter,timeConverter_timeNanoseconds,INPUT,java.math.BigDecimal,1 +units-converter,transferRateConverter_bitTransferRateValue,INPUT,java.math.BigDecimal,1 +units-converter,dataSizeConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.common.LocaleContainer,zh-MO +units-converter,transferRateConverter_roundingMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.unitconverter.MathContextUnitConverter$RoundingMode,HALF_DOWN +units-converter,transferRateConverter_useCombinedAbbreviationNotation,CONFIGURATION,kotlin.Boolean,false +units-converter,dataSizeConverter_showLargeDataUnits,CONFIGURATION,kotlin.Boolean,true +units-converter,transferRateConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +text-filter,filteringMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextFilterTransformer$FilteringMode,NOT_CONTAINING +text-filter,text-filter-source-input-softWraps,CONFIGURATION,kotlin.Boolean,false +text-filter,liveTransformation,CONFIGURATION,kotlin.Boolean,false +text-filter,text-filter-result-output-softWraps,CONFIGURATION,kotlin.Boolean,false +text-filter,sourceText,INPUT,kotlin.String,FFpu3QLxC6KyzMzzWhQl +text-filter,text-filter-result-output-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-filter,tokenSelectionMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextFilterTransformer$TokenMode,WORD +text-filter,text-filter-source-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-filter,filteringRegexModeOptions,CONFIGURATION,kotlin.Int,1 +text-filter,filteringNotContainingModeText,INPUT,kotlin.String,MKJwePY4J9mYgfwjX5K1 +text-filter,filteringContainingModeText,INPUT,kotlin.String,DFSaExC8uSoAKdMBquKd +text-filter,filteringRegexModeText,INPUT,kotlin.String,WsP6NGq2ySUyfTyjRBhQ +text-filter,text-filter-source-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-filter,text-filter-result-output-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ulid-generator,ulid-generator-bulk-generation-softWraps,CONFIGURATION,kotlin.Boolean,false +ulid-generator,generateMonotonicUlid,CONFIGURATION,kotlin.Boolean,true +ulid-generator,ulid-generator-bulk-generation-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ulid-generator,individualTime,CONFIGURATION,kotlin.Long,1745046780032 +ulid-generator,ulidFormat,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.UlidGenerator$UlidFormat,TO_LOWERCASE +ulid-generator,useIndividualTime,CONFIGURATION,kotlin.Boolean,true +ulid-generator,ulid-generator-bulk-generation-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +unarchiver,lastSelectedTargetDirectoryPath,CONFIGURATION,kotlin.String,RlXWUKhgSLzkhYMBEzGf +unarchiver,archiveTreeSortingMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.other.Unarchiver$SortingMode,FILENAME_DESC +unarchiver,showArchiveNodeTotalNumberOfChildren,CONFIGURATION,kotlin.Boolean,false +unarchiver,createParentDirectories,CONFIGURATION,kotlin.Boolean,false +unarchiver,clearTargetDirectory,CONFIGURATION,kotlin.Boolean,true +unarchiver,openFileInEditorOnDoubleClick,CONFIGURATION,kotlin.Boolean,false +unarchiver,preserveFileAttributes,CONFIGURATION,kotlin.Boolean,false +unarchiver,openTargetDirectoryAfterExtraction,CONFIGURATION,kotlin.Boolean,false +unarchiver,lastSelectedOpenedDirectoryPath,CONFIGURATION,kotlin.String,h4sYiYVc41yihl9KVSYD +unarchiver,preserveDirectoryStructure,CONFIGURATION,kotlin.Boolean,false +unarchiver,createArchiveFilenameSubDirectory,CONFIGURATION,kotlin.Boolean,false +unarchiver,showArchiveNodeUncompressedSize,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,base32-encoder-decoder-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,base32-encoder-decoder-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,base32-encoder-decoder-target-softWraps,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,sourceText,INPUT,kotlin.String,txThpTvavAEQn4akbEaq +base32-encoder-decoder,base32-encoder-decoder-source-softWraps,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,base32-encoder-decoder-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,base32-encoder-decoder-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-format-converter,text-format-converter-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-format-converter,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-format-converter,sourceText,INPUT,kotlin.String,dcbaAairAWyeBeLWjDsf +text-format-converter,text-format-converter-source-softWraps,CONFIGURATION,kotlin.Boolean,false +text-format-converter,text-format-converter-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-format-converter,secondLanguage,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.CodeFormattingConverter$Language,XML +text-format-converter,text-format-converter-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-format-converter,firstLanguage,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.CodeFormattingConverter$Language,YAML +text-format-converter,text-format-converter-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-format-converter,text-format-converter-target-softWraps,CONFIGURATION,kotlin.Boolean,false +qr-code-generator,qr-code-generator-content-softWraps,CONFIGURATION,kotlin.Boolean,false +qr-code-generator,PDF_417-minRows,CONFIGURATION,kotlin.Int,2 +qr-code-generator,CODE_39-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODE_128-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,contentText,INPUT,kotlin.String,SsJEAZq0jjoyPKUfCSw4 +qr-code-generator,QR_CODE-margin,CONFIGURATION,kotlin.Int,1 +qr-code-generator,PDF_417-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_EAN_EXTENSION-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,PDF_417-compactionModeType,CONFIGURATION,com.google.zxing.pdf417.encoder.Compaction,TEXT +qr-code-generator,ITF-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,PDF_417-insertEcis,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,CODE_93-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,ITF-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODE_39-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,EAN_13-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,AZTEC-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,PDF_417-compactMode,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,CODABAR-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,UPC_A-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,PDF_417-maxColumns,CONFIGURATION,kotlin.Int,51 +qr-code-generator,QR_CODE-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,DATA_MATRIX-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,PDF_417-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,QR_CODE-maskPattern,CONFIGURATION,kotlin.Int,0 +qr-code-generator,format,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$Format,DATA_MATRIX +qr-code-generator,foregroundColor,CONFIGURATION,com.intellij.ui.JBColor,-16777152 +qr-code-generator,DATA_MATRIX-height,CONFIGURATION,kotlin.Int,251 +qr-code-generator,EAN_8-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODE_39-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,CODE_93-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODE_128-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,QR_CODE-width,CONFIGURATION,kotlin.Int,201 +qr-code-generator,CODABAR-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,AZTEC-layers,CONFIGURATION,kotlin.Int,1 +qr-code-generator,EAN_13-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODABAR-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,ITF-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,AZTEC-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODABAR-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,qr-code-generator-content-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,CODE_93-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,UPC_EAN_EXTENSION-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,QR_CODE-compactMode,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,UPC_E-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,CODE_128-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,DATA_MATRIX-gs1,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,PDF_417-setDimensions,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,UPC_EAN_EXTENSION-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,UPC_E-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,QR_CODE-height,CONFIGURATION,kotlin.Int,201 +qr-code-generator,EAN_8-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,liveGeneration,CONFIGURATION,kotlin.Boolean,false +qr-code-generator,DATA_MATRIX-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_A-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,DATA_MATRIX-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,CODE_128-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_A-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,AZTEC-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,EAN_8-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,backgroundColor,CONFIGURATION,com.intellij.ui.JBColor,-16777177 +qr-code-generator,DATA_MATRIX-forceC40,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,EAN_13-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,UPC_A-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,DATA_MATRIX-symbolShape,CONFIGURATION,com.google.zxing.datamatrix.encoder.SymbolShapeHint,FORCE_SQUARE +qr-code-generator,QR_CODE-gs1,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,QR_CODE-version,CONFIGURATION,kotlin.Int,1 +qr-code-generator,CODE_93-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,EAN_13-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_EAN_EXTENSION-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_E-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,PDF_417-margin,CONFIGURATION,kotlin.Int,6 +qr-code-generator,UPC_E-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,AZTEC-height,CONFIGURATION,kotlin.Int,251 +qr-code-generator,PDF_417-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,qr-code-generator-content-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,CODE_39-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,PDF_417-minColumns,CONFIGURATION,kotlin.Int,2 +qr-code-generator,EAN_8-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,ITF-errorCorrection,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.BarcodeGenerator$ErrorCorrection,L +qr-code-generator,DATA_MATRIX-compactMode,CONFIGURATION,kotlin.Boolean,true +nano-id-generator,nano-id-generator-bulk-generation-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +nano-id-generator,nano-id-generator-bulk-generation-softWraps,CONFIGURATION,kotlin.Boolean,false +nano-id-generator,nano-id-generator-bulk-generation-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,mime-base64-encoder-decoder-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,mime-base64-encoder-decoder-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,mime-base64-encoder-decoder-target-softWraps,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,mime-base64-encoder-decoder-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,sourceText,INPUT,kotlin.String,sb48MAdQAy73ow7pVuyS +mime-base64-encoder-decoder,mime-base64-encoder-decoder-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,targetText,INPUT,kotlin.String,c2I0OE1BZFFBeTczb3c3cFZ1eVM= +mime-base64-encoder-decoder,mime-base64-encoder-decoder-source-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,regular-expression-matcher-extraction-result-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,substitutionPattern,INPUT,kotlin.String,6Ns9kyWkdqrD5yVb1X8g +regular-expression-matcher,extractionPattern,INPUT,kotlin.String,p2jImeDjeRjxDfg2Rc5i +regular-expression-matcher,regular-expression-matcher-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,inputText,INPUT,kotlin.String,tuU35mYt0CriMtESTY7k +regular-expression-matcher,regular-expression-matcher-substitution-result-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,regular-expression-matcher-substitution-result-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,regular-expression-matcher-extraction-result-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,regular-expression-matcher-extraction-result-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,regular-expression-matcher-input-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,regexOption,CONFIGURATION,kotlin.Int,1 +regular-expression-matcher,regular-expression-matcher-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,regexText,INPUT,kotlin.String,xHAQOIkHJJnoKzRx5cB2 +regular-expression-matcher,regular-expression-matcher-substitution-result-softWraps,CONFIGURATION,kotlin.Boolean,false +color-picker,selectedColor,INPUT,com.intellij.ui.JBColor,-16777044 +uuid-generator,UUIDv1IndividualMacAddress,CONFIGURATION,kotlin.String,j6DdgkIpyLnXBhcNgoq5 +uuid-generator,UUIDv5Name,CONFIGURATION,kotlin.String,eB1l0BDKsXrdx9KVexoS +uuid-generator,UUIDv6MacAddressGenerationMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.MacAddressBasedUuidGenerator$MacAddressGenerationMode,INDIVIDUAL +uuid-generator,UUIDv6IndividualMacAddress,CONFIGURATION,kotlin.String,JArHeMWAFiGDHCm9XsjG +uuid-generator,UUIDv5IndividualNamespace,CONFIGURATION,kotlin.String,vKTB7uv9b8CDI46xM1QF +uuid-generator,UUIDv5PredefinedNamespace,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.NamespaceAndNameBasedUuidGenerator$PredefinedNamespace,URL +uuid-generator,UUIDv3Name,CONFIGURATION,kotlin.String,ti8xlnjp5t4GrtE56iYT +uuid-generator,UUIDv1LocalInterface,CONFIGURATION,kotlin.String,e0fqhcmtEq8S6xw0U25V +uuid-generator,version,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.UuidVersion,V5 +uuid-generator,uuid-generator-bulk-generation-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +uuid-generator,UUIDv3PredefinedNamespace,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.NamespaceAndNameBasedUuidGenerator$PredefinedNamespace,URL +uuid-generator,uuid-generator-bulk-generation-softWraps,CONFIGURATION,kotlin.Boolean,false +uuid-generator,UUIDv5NamespaceMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.NamespaceAndNameBasedUuidGenerator$NamespaceMode,INDIVIDUAL +uuid-generator,UUIDv3IndividualNamespace,CONFIGURATION,kotlin.String,DrUFNUBPY7dwhg6V4zhR +uuid-generator,UUIDv6LocalInterface,CONFIGURATION,kotlin.String,9Lsj4bOgv6pT6Ws1ogSC +uuid-generator,uuid-generator-bulk-generation-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +uuid-generator,UUIDv1MacAddressGenerationMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.MacAddressBasedUuidGenerator$MacAddressGenerationMode,INDIVIDUAL +uuid-generator,UUIDv3NamespaceMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.uuid.NamespaceAndNameBasedUuidGenerator$NamespaceMode,INDIVIDUAL +text-case-transformer,liveTransformation,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,text-case-transformer-source-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,sourceText,INPUT,kotlin.String,Uc1nVFwwQRhoKV5cnPWu +text-case-transformer,text-case-transformer-source-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,individualDelimiter,CONFIGURATION,kotlin.String,rIhpjGoZxy2IKjl2Tfec +text-case-transformer,text-case-transformer-result-output-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,outputTextCase,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextCaseTransformer$TextCase,PASCAL_SNAKE_CASE +text-case-transformer,inputTextCase,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextCaseTransformer$TextCase,PASCAL_CASE +text-case-transformer,text-case-transformer-source-input-softWraps,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,text-case-transformer-result-output-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,originalParsingMode,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextCaseTransformer$OriginalParsingMode,FIXED_TEXT_CASE +text-case-transformer,text-case-transformer-result-output-softWraps,CONFIGURATION,kotlin.Boolean,false +lorem-ipsum-generator,startWithLoremIpsum,CONFIGURATION,kotlin.Boolean,false +lorem-ipsum-generator,maxWordsInBullet,CONFIGURATION,kotlin.Int,31 +lorem-ipsum-generator,lorem-ipsum-generator-generated-text-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +lorem-ipsum-generator,lorem-ipsum-generator-generated-text-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +lorem-ipsum-generator,numberOfValues,CONFIGURATION,kotlin.Int,10 +lorem-ipsum-generator,lorem-ipsum-generator-generated-text-softWraps,CONFIGURATION,kotlin.Boolean,false +lorem-ipsum-generator,minWordsInParagraph,CONFIGURATION,kotlin.Int,21 +lorem-ipsum-generator,maxWordsInParagraph,CONFIGURATION,kotlin.Int,101 +lorem-ipsum-generator,minWordsInBullet,CONFIGURATION,kotlin.Int,11 +lorem-ipsum-generator,generatedTextKind,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.generator.LoremIpsumGenerator$TextMode,WORDS +url-base64-encoder-decoder,url-base64-encoder-decoder-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,url-base64-encoder-decoder-source-softWraps,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,url-base64-encoder-decoder-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,sourceText,INPUT,kotlin.String,RT1nGZUNQ0mjDTAuR4KE +url-base64-encoder-decoder,url-base64-encoder-decoder-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,url-base64-encoder-decoder-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,url-base64-encoder-decoder-target-softWraps,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,csv-text-escape-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,csv-text-escape-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,csv-text-escape-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,sourceText,INPUT,kotlin.String,6uQR75PZRGXRv5XElMX5 +csv-text-escape,csv-text-escape-target-softWraps,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,csv-text-escape-source-softWraps,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,csv-text-escape-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,targetText,INPUT,kotlin.String,6uQR75PZRGXRv5XElMX5 +sql-formatting,uppercase,CONFIGURATION,kotlin.Boolean,false +sql-formatting,sql-formatting-result-output-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +sql-formatting,dialect,CONFIGURATION,com.github.vertical_blank.sqlformatter.languages.Dialect,PlSql +sql-formatting,linesBetweenQueries,CONFIGURATION,kotlin.Int,2 +sql-formatting,sql-formatting-source-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +sql-formatting,liveTransformation,CONFIGURATION,kotlin.Boolean,false +sql-formatting,indentSpaces,CONFIGURATION,kotlin.Int,3 +sql-formatting,sourceText,INPUT,kotlin.String,6Zwrg7p5OSjATnMCkPjA +sql-formatting,maxColumnLength,CONFIGURATION,kotlin.Int,31 +sql-formatting,sql-formatting-result-output-softWraps,CONFIGURATION,kotlin.Boolean,false +sql-formatting,sql-formatting-source-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +sql-formatting,sql-formatting-source-input-softWraps,CONFIGURATION,kotlin.Boolean,false +sql-formatting,sql-formatting-result-output-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,hashing-transformer-result-output-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,hashing-transformer-source-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,liveTransformation,CONFIGURATION,kotlin.Boolean,false +hashing-transformer,sourceText,INPUT,kotlin.String,Rr3RuG1SjsCiIAFkSSRP +hashing-transformer,hashing-transformer-result-output-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,hashing-transformer-source-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,hashing-transformer-source-input-softWraps,CONFIGURATION,kotlin.Boolean,false +hashing-transformer,algorithm,CONFIGURATION,kotlin.String,SHAKE256-512 +hashing-transformer,hashing-transformer-result-output-softWraps,CONFIGURATION,kotlin.Boolean,false +line-breaks-encoder-decoder,line-breaks-encoder-decoder-target-softWraps,CONFIGURATION,kotlin.Boolean,false +line-breaks-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +line-breaks-encoder-decoder,lineBreakDecoding,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.converter.LineBreaksEncoderDecoder$LineBreak,LF +line-breaks-encoder-decoder,line-breaks-encoder-decoder-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +line-breaks-encoder-decoder,line-breaks-encoder-decoder-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +line-breaks-encoder-decoder,sourceText,INPUT,kotlin.String,KziyJYWSw7otO85hVEH0 +line-breaks-encoder-decoder,line-breaks-encoder-decoder-source-softWraps,CONFIGURATION,kotlin.Boolean,false +line-breaks-encoder-decoder,line-breaks-encoder-decoder-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +line-breaks-encoder-decoder,line-breaks-encoder-decoder-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,xml-text-escape-source-softWraps,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,xml-text-escape-source-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,xml-text-escape-source-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,xml-text-escape-target-softWraps,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,sourceText,INPUT,kotlin.String,mVsqr1OWSsY6nZ3IGz5e +xml-text-escape,xml-text-escape-target-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,xml-text-escape-target-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,sortingOrder,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextSortingTransformer$SortingOrder,WORD_LENGTH +text-sorting-transformer,liveTransformation,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,sortedIndividualJoinWordsDelimiter,CONFIGURATION,kotlin.String,SnzsijF5VCKyD73l8OkS +text-sorting-transformer,reverseOrder,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,sourceText,INPUT,kotlin.String,1wMAao7rbgbm1U4QDks1 +text-sorting-transformer,text-sorting-transformer-result-output-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,text-sorting-transformer-source-input-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,sortedJoinWordsDelimiter,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextSortingTransformer$WordsDelimiter,SPACE +text-sorting-transformer,removeDuplicates,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,text-sorting-transformer-result-output-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,trimWords,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,text-sorting-transformer-source-input-softWraps,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,unsortedIndividualSplitWordsDelimiter,CONFIGURATION,kotlin.String,zxHRnKc2W0jAVeSLBazk +text-sorting-transformer,unsortedPredefinedDelimiter,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin._internal.tool.ui.transformer.TextSortingTransformer$WordsDelimiter,SPACE +text-sorting-transformer,caseInsensitive,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,removeBlankWords,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,text-sorting-transformer-result-output-softWraps,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,text-sorting-transformer-source-input-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +certificates-download,followRedirects,CONFIGURATION,kotlin.Boolean,false +certificates-download,allowInsecureConnection,CONFIGURATION,kotlin.Boolean,true +certificates-download,url,INPUT,kotlin.String,8mi2IhOI3rbVSiR7RxIc diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/6.3.0/instance-settings-persisted-state.xml b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/6.3.0/instance-settings-persisted-state.xml new file mode 100644 index 00000000..fb6df43d --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/6.3.0/instance-settings-persisted-state.xml @@ -0,0 +1,607 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/expected-configuration-properties.csv b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/expected-configuration-properties.csv new file mode 100644 index 00000000..7b8c487e --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/expected-configuration-properties.csv @@ -0,0 +1,548 @@ +developerToolId,propertyKey,propertyType,propertyValueTypeName,propertyValue +ascii-art,asciiArtOutput-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ascii-art,asciiArtOutput-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-art,asciiArtOutput-editor-softWraps,CONFIGURATION,kotlin.Boolean,true +ascii-art,selectedFontFileName,CONFIGURATION,kotlin.String,slant.flf +ascii-art,textInput,INPUT,kotlin.String,Qr6OImM2yhC36gPVBvAa +ascii-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,sourceFile,INPUT,kotlin.String,22VcBEJkq5a5ZvjsPwsX +ascii-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +ascii-encoder-decoder,sourceText,INPUT,kotlin.String,3pi0sWQx62RVpvZbsYKj +ascii-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ascii-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +ascii-encoder-decoder,targetFile,INPUT,kotlin.String,8UsrqyCv0iJtNK4Q3qGX +ascii-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +ascii-encoder-decoder,targetText,INPUT,kotlin.String,3pi0sWQx62RVpvZbsYKj +base32-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,sourceFile,INPUT,kotlin.String,CLZecyHjiemHs6S9enkO +base32-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +base32-encoder-decoder,sourceText,INPUT,kotlin.String,a8jkmiSP3NRPPyDryrjt +base32-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base32-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +base32-encoder-decoder,targetFile,INPUT,kotlin.String,6heotp7RETnB7eCQEFMn +base32-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +base32-encoder-decoder,targetText,INPUT,kotlin.String,ME4GU23NNFJVAM2OKJIFA6KEOJ4XE2TU +base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,sourceFile,INPUT,kotlin.String,qkBl72u5j1kd4gSbfccH +base64-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +base64-encoder-decoder,sourceText,INPUT,kotlin.String,2dJFvpDKggJpNQ0YJXNR +base64-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +base64-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +base64-encoder-decoder,targetFile,INPUT,kotlin.String,V5Y7q3KgKfUOmHDmiVIF +base64-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +base64-encoder-decoder,targetText,INPUT,kotlin.String,MmRKRnZwREtnZ0pwTlEwWUpYTlI= +certificates-download,allowInsecureConnection,CONFIGURATION,kotlin.Boolean,true +certificates-download,followRedirects,CONFIGURATION,kotlin.Boolean,false +certificates-download,url,INPUT,kotlin.String,klQRyod75tYJisLYP7A7 +cli-command-converter,lineBreakDelimiter,CONFIGURATION,kotlin.String,Ku2IaXas5zB8eWLjQGY5 +cli-command-converter,liveConversion,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,sourceFile,INPUT,kotlin.String,291CdtNiHFtAF91dO0e4 +cli-command-converter,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +cli-command-converter,sourceText,INPUT,kotlin.String,ogp9TFTc5R7d7O7zK3Ld +cli-command-converter,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +cli-command-converter,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +cli-command-converter,targetFile,INPUT,kotlin.String,xgRfhD9ro0Eyq9pLxm4L +cli-command-converter,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +cli-command-converter,targetText,INPUT,kotlin.String,ogp9TFTc5R7d7O7zK3Ld +code-style-formatting,languageId,CONFIGURATION,kotlin.String,XML +code-style-formatting,liveTransformation,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,result-output-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,result-output-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,result-output-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,source-input-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,source-input-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +code-style-formatting,source-input-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +code-style-formatting,sourceText,INPUT,kotlin.String, +color-picker,decimalPlaces,CONFIGURATION,kotlin.Int,3 +color-picker,selectedColor,INPUT,com.intellij.ui.JBColor,-16777177 +csv-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,sourceFile,INPUT,kotlin.String,0MpBQneVUJVdtLFkwkYR +csv-text-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +csv-text-escape,sourceText,INPUT,kotlin.String,4QnMMzLS6UUqg1xs73CY +csv-text-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +csv-text-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +csv-text-escape,targetFile,INPUT,kotlin.String,nfnEgXb8qCdMb3xZ8itx +csv-text-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +csv-text-escape,targetText,INPUT,kotlin.String,4QnMMzLS6UUqg1xs73CY +date-time-converter,formattedIndividual,CONFIGURATION,kotlin.Boolean,true +date-time-converter,formattedIndividualFormat,CONFIGURATION,kotlin.String,1YuUfvqrjPCSCOSXhRDP +date-time-converter,formattedLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer,bgc-IN +date-time-converter,formattedStandardFormat,CONFIGURATION,DatetimeConverter-StandardFormat,ISO_8601_DATE +date-time-converter,formattedStandardFormatAddOffset,CONFIGURATION,kotlin.Boolean,false +date-time-converter,formattedStandardFormatAddTimeZone,CONFIGURATION,kotlin.Boolean,true +date-time-converter,timeZoneId,CONFIGURATION,kotlin.String,America/Araguaina +escape-sequence-escaper-unescaper,backslashsEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,doubleQuotesEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,lineBreaksEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,lineBreaksEscapeSequence,CONFIGURATION,EscapeSequencesEncoderDecoder-LineBreak,LF +escape-sequence-escaper-unescaper,liveConversion,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,singleQuotesEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +escape-sequence-escaper-unescaper,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +escape-sequence-escaper-unescaper,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,sourceFile,INPUT,kotlin.String,E4fEkYac7JDhJP5wCN4Y +escape-sequence-escaper-unescaper,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +escape-sequence-escaper-unescaper,sourceText,INPUT,kotlin.String,7cbR7EQhCpTMcTSsyQfQ +escape-sequence-escaper-unescaper,tabsEncodingEnabled,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +escape-sequence-escaper-unescaper,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +escape-sequence-escaper-unescaper,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +escape-sequence-escaper-unescaper,targetFile,INPUT,kotlin.String,PA5OxhwEt3vbMw0CLgo0 +escape-sequence-escaper-unescaper,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +escape-sequence-escaper-unescaper,targetText,INPUT,kotlin.String,7cbR7EQhCpTMcTSsyQfQ +hashing-transformer,algorithm,CONFIGURATION,kotlin.String,BLAKE2S-128 +hashing-transformer,liveConversion,CONFIGURATION,kotlin.Boolean,false +hashing-transformer,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +hashing-transformer,sourceFile,INPUT,kotlin.String,Aj1aVZ1CWz8r0QIIr8tR +hashing-transformer,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +hashing-transformer,sourceText,INPUT,kotlin.String,jekpkkrI5zat5dXxudXQ +hashing-transformer,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hashing-transformer,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +hashing-transformer,targetFile,INPUT,kotlin.String,RqILHIRmaoBo91slpbyA +hashing-transformer,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +hmac-transformer,algorithm,CONFIGURATION,kotlin.String,HMACMD4 +hmac-transformer,liveConversion,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,secretKey,SENSITIVE,kotlin.String,TSY0bTbx5gk278QhMQ2c +hmac-transformer,secretKeyEncodingMode,CONFIGURATION,HmacTransformer-SecretKeyEncodingMode,BASE32 +hmac-transformer,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,sourceFile,INPUT,kotlin.String,ThgiLyjidQOogFFaKD8Z +hmac-transformer,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +hmac-transformer,sourceText,INPUT,kotlin.String,1zN0a7jropsqMRFwAipw +hmac-transformer,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +hmac-transformer,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +hmac-transformer,targetFile,INPUT,kotlin.String,OIO1Fvx1358QiiwWFHAq +hmac-transformer,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +html-entities-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,sourceFile,INPUT,kotlin.String,4fmJVyJo8nwssz09L1DA +html-entities-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +html-entities-escape,sourceText,INPUT,kotlin.String,CXTBzmt03hVEkZV7cOrv +html-entities-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +html-entities-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +html-entities-escape,targetFile,INPUT,kotlin.String,VmEdJkMTUnlLfKjoOSex +html-entities-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +html-entities-escape,targetText,INPUT,kotlin.String,CXTBzmt03hVEkZV7cOrv +java-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +java-text-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +java-text-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +java-text-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +java-text-escape,sourceFile,INPUT,kotlin.String,smeUYDLI7mnhJyoTvrrp +java-text-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +java-text-escape,sourceText,INPUT,kotlin.String,e6Ky0YsWUHgIdk5W6c3S +java-text-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +java-text-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +java-text-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +java-text-escape,targetFile,INPUT,kotlin.String,6ELxxpJ3wI0Dz7Hu7Yuq +java-text-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +java-text-escape,targetText,INPUT,kotlin.String,e6Ky0YsWUHgIdk5W6c3S +json-path,contentText,INPUT,kotlin.String,iAPwuQ2NQq1MN1WIQ9yg +json-path,formatResult,CONFIGURATION,kotlin.Boolean,false +json-path,liveTransformation,CONFIGURATION,kotlin.Boolean,false +json-path,result-output-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-path,result-output-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-path,result-output-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-path,source-input-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-path,source-input-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-path,source-input-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-path,sourceText,INPUT,kotlin.String,anRIH0MflLFYPdx2XDc2 +json-schema-validator,data-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,data-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,data-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,dataText,INPUT,kotlin.String,YDp766m64GTnfawwcIsn +json-schema-validator,liveValidation,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,schema-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,schema-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-schema-validator,schema-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-schema-validator,schemaText,INPUT,kotlin.String,aMC1x4WZUxHpVrK2gySI +json-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +json-text-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-text-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-text-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-text-escape,sourceFile,INPUT,kotlin.String,Oa477rWMn7NMnYyLSvLI +json-text-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +json-text-escape,sourceText,INPUT,kotlin.String,JOhNMnvFXiKwXyDUSs7b +json-text-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +json-text-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +json-text-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +json-text-escape,targetFile,INPUT,kotlin.String,bYpXARW9zzWemzFLeQSB +json-text-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +json-text-escape,targetText,INPUT,kotlin.String,JOhNMnvFXiKwXyDUSs7b +jwt-encoder-decoder,algorithm,CONFIGURATION,JwtEncoderDecoder-SignatureAlgorithm,HMAC512 +jwt-encoder-decoder,encoded-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,encoded-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,encoded-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,encodedText,INPUT,kotlin.String,eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.ANCf_8p1AE4ZQs7QuqGAyyfTEgYrKSjKWkhBk5cIn1_2QVr2jEjmM-1tu7EgnyOf_fAsvdFXva8Sv05iTGzETg +jwt-encoder-decoder,header-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,header-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,header-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,headerText,INPUT,kotlin.String,"{ + ""alg"": ""HS512"", + ""typ"": ""JWT"" +}" +jwt-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,payload-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,payload-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +jwt-encoder-decoder,payload-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +jwt-encoder-decoder,payloadText,INPUT,kotlin.String,"{ + ""sub"": ""1234567890"", + ""name"": ""John Doe"", + ""admin"": true, + ""iat"": 1516239022 +}" +jwt-encoder-decoder,privateKey,SENSITIVE,kotlin.String,h9PYLRd62MS1w23slmgF +jwt-encoder-decoder,secret,SENSITIVE,kotlin.String,puOrCSGoDSGuXsWSA6mI +jwt-encoder-decoder,secretKeyEncodingMode,CONFIGURATION,JwtEncoderDecoder-SecretKeyEncodingMode,BASE32 +jwt-encoder-decoder,signingKeyValidation,CONFIGURATION,kotlin.Boolean,true +lorem-ipsum-generator,generated-text-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +lorem-ipsum-generator,generated-text-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +lorem-ipsum-generator,generated-text-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +lorem-ipsum-generator,generatedTextKind,CONFIGURATION,LoremIpsumGenerator-TextMode,WORDS +lorem-ipsum-generator,maxWordsInBullet,CONFIGURATION,kotlin.Int,31 +lorem-ipsum-generator,maxWordsInParagraph,CONFIGURATION,kotlin.Int,101 +lorem-ipsum-generator,minWordsInBullet,CONFIGURATION,kotlin.Int,11 +lorem-ipsum-generator,minWordsInParagraph,CONFIGURATION,kotlin.Int,21 +lorem-ipsum-generator,numberOfValues,CONFIGURATION,kotlin.Int,10 +lorem-ipsum-generator,startWithLoremIpsum,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,sourceFile,INPUT,kotlin.String,77aYbj3eun4qO4nCoAlW +mime-base64-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +mime-base64-encoder-decoder,sourceText,INPUT,kotlin.String,CdxqRq1bxou01JcFuGbQ +mime-base64-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +mime-base64-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +mime-base64-encoder-decoder,targetFile,INPUT,kotlin.String,QQx22uT9iqHNxAgs2KuL +mime-base64-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +mime-base64-encoder-decoder,targetText,INPUT,kotlin.String,Q2R4cVJxMWJ4b3UwMUpjRnVHYlE= +nano-id-generator,bulk-generation-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +nano-id-generator,bulk-generation-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +nano-id-generator,bulk-generation-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +notes,content-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +notes,content-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +notes,content-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +notes,test,INPUT,kotlin.String,Ap5c39Hp3QKbEhOFG1Ai +password-generator,addDigits,CONFIGURATION,kotlin.Boolean,false +password-generator,addSymbols,CONFIGURATION,kotlin.Boolean,false +password-generator,bulk-generation-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +password-generator,bulk-generation-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +password-generator,bulk-generation-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +password-generator,length,CONFIGURATION,kotlin.Int,31 +password-generator,lettersMode,CONFIGURATION,PasswordGenerator-LettersMode,ASCII_ALPHABET_ONLY_LOWERCASE +password-generator,symbols,CONFIGURATION,kotlin.String,4ZEojE03mO3wDnVd78Vo +qr-code-generator,AZTEC-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,AZTEC-height,CONFIGURATION,kotlin.Int,251 +qr-code-generator,AZTEC-layers,CONFIGURATION,kotlin.Int,1 +qr-code-generator,AZTEC-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,AZTEC-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODABAR-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,CODABAR-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODABAR-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODABAR-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODE_128-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,CODE_128-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODE_128-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODE_128-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODE_39-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,CODE_39-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODE_39-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODE_39-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,CODE_93-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,CODE_93-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,CODE_93-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,CODE_93-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,DATA_MATRIX-compactMode,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,DATA_MATRIX-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,DATA_MATRIX-forceC40,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,DATA_MATRIX-gs1,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,DATA_MATRIX-height,CONFIGURATION,kotlin.Int,251 +qr-code-generator,DATA_MATRIX-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,DATA_MATRIX-symbolShape,CONFIGURATION,QrCode-SymbolShapeHint,FORCE_SQUARE +qr-code-generator,DATA_MATRIX-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,EAN_13-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,EAN_13-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,EAN_13-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,EAN_13-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,EAN_8-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,EAN_8-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,EAN_8-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,EAN_8-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,ITF-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,ITF-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,ITF-margin,CONFIGURATION,kotlin.Int,11 +qr-code-generator,ITF-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,PDF_417-compactMode,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,PDF_417-compactionModeType,CONFIGURATION,QrCode-Compaction,TEXT +qr-code-generator,PDF_417-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,PDF_417-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,PDF_417-insertEcis,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,PDF_417-margin,CONFIGURATION,kotlin.Int,6 +qr-code-generator,PDF_417-maxColumns,CONFIGURATION,kotlin.Int,51 +qr-code-generator,PDF_417-minColumns,CONFIGURATION,kotlin.Int,2 +qr-code-generator,PDF_417-minRows,CONFIGURATION,kotlin.Int,2 +qr-code-generator,PDF_417-setDimensions,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,PDF_417-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,QR_CODE-compactMode,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,QR_CODE-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,QR_CODE-gs1,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,QR_CODE-height,CONFIGURATION,kotlin.Int,201 +qr-code-generator,QR_CODE-margin,CONFIGURATION,kotlin.Int,1 +qr-code-generator,QR_CODE-maskPattern,CONFIGURATION,kotlin.Int,0 +qr-code-generator,QR_CODE-version,CONFIGURATION,kotlin.Int,1 +qr-code-generator,QR_CODE-width,CONFIGURATION,kotlin.Int,201 +qr-code-generator,UPC_A-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,UPC_A-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,UPC_A-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,UPC_A-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_E-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,UPC_E-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,UPC_E-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,UPC_E-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,UPC_EAN_EXTENSION-errorCorrection,CONFIGURATION,BarcodeGenerator-ErrorCorrection,L +qr-code-generator,UPC_EAN_EXTENSION-height,CONFIGURATION,kotlin.Int,51 +qr-code-generator,UPC_EAN_EXTENSION-margin,CONFIGURATION,kotlin.Int,2 +qr-code-generator,UPC_EAN_EXTENSION-width,CONFIGURATION,kotlin.Int,251 +qr-code-generator,backgroundColor,CONFIGURATION,com.intellij.ui.JBColor,-16777067 +qr-code-generator,content-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,content-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +qr-code-generator,content-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +qr-code-generator,contentText,INPUT,kotlin.String,O0hh2q5hqbNLHrmzKoX5 +qr-code-generator,foregroundColor,CONFIGURATION,com.intellij.ui.JBColor,-16777194 +qr-code-generator,format,CONFIGURATION,BarcodeGenerator-Format,DATA_MATRIX +qr-code-generator,liveGeneration,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,extraction-result-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,extraction-result-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,extraction-result-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,extractionPattern,INPUT,kotlin.String,skNapewXe2EBc9vUL91H +regular-expression-matcher,input-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,input-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,input-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,inputText,INPUT,kotlin.String,4HErDteJVtq22JWjFtvc +regular-expression-matcher,regexOption,CONFIGURATION,kotlin.Int,1 +regular-expression-matcher,regexText,INPUT,kotlin.String,SBYXpjoV5ikRMjUyugIs +regular-expression-matcher,substitution-result-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,substitution-result-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +regular-expression-matcher,substitution-result-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +regular-expression-matcher,substitutionPattern,INPUT,kotlin.String,b6yCcb5vVgaYH4Vi2J09 +sql-formatting,dialect,CONFIGURATION,SqlFormatter-Dialect,PlSql +sql-formatting,indentSpaces,CONFIGURATION,kotlin.Int,3 +sql-formatting,linesBetweenQueries,CONFIGURATION,kotlin.Int,2 +sql-formatting,liveConversion,CONFIGURATION,kotlin.Boolean,false +sql-formatting,maxColumnLength,CONFIGURATION,kotlin.Int,31 +sql-formatting,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +sql-formatting,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +sql-formatting,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +sql-formatting,sourceFile,INPUT,kotlin.String,751yRm6J7afAT5Gqf04D +sql-formatting,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +sql-formatting,sourceText,INPUT,kotlin.String,69jx5PWRAuZfFxEVtWfU +sql-formatting,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +sql-formatting,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +sql-formatting,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +sql-formatting,targetFile,INPUT,kotlin.String,sVFCdWmgil8b7zIDxYfo +sql-formatting,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +sql-formatting,uppercase,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,individualDelimiter,CONFIGURATION,kotlin.String,7Zld2FORIbVBdbFXFwoZ +text-case-transformer,inputTextCase,CONFIGURATION,TextCaseTransformer-TextCase,PASCAL_CASE +text-case-transformer,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,originalParsingMode,CONFIGURATION,TextCaseTransformer-OriginalParsingMode,FIXED_TEXT_CASE +text-case-transformer,outputTextCase,CONFIGURATION,TextCaseTransformer-TextCase,PASCAL_SNAKE_CASE +text-case-transformer,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,sourceFile,INPUT,kotlin.String,a7uihZbrGSmytJZZVU8y +text-case-transformer,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-case-transformer,sourceText,INPUT,kotlin.String,L0ixXkUF28xCKKfBRrl2 +text-case-transformer,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-case-transformer,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-case-transformer,targetFile,INPUT,kotlin.String,HddkZYby2q2JBCkL8fVM +text-case-transformer,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-diff,firstText,INPUT,kotlin.String,jYP0SYIM6vPNrARg7CGU +text-diff,secondText,INPUT,kotlin.String,UPrrArXR0UXAMeb3et3V +text-filter,filteringContainingModeText,INPUT,kotlin.String,SsIxPcxxu3VAHOmfkeuM +text-filter,filteringMode,CONFIGURATION,TextFilterTransformer-FilteringMode,NOT_CONTAINING +text-filter,filteringNotContainingModeText,INPUT,kotlin.String,l8ffJAzFb6vonkvBSZDq +text-filter,filteringRegexModeOptions,CONFIGURATION,kotlin.Int,1 +text-filter,filteringRegexModeText,INPUT,kotlin.String,VEZcr3DyNMF5aHHfcOvV +text-filter,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-filter,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-filter,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-filter,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-filter,sourceFile,INPUT,kotlin.String,9uwoOPKivgy34CJYOn4Z +text-filter,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-filter,sourceText,INPUT,kotlin.String,6NQBOK7ESkwrNGnYgT7T +text-filter,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-filter,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-filter,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-filter,targetFile,INPUT,kotlin.String,zGGIRgfnVMRf2cAUi4yu +text-filter,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-filter,tokenSelectionMode,CONFIGURATION,TextFilterTransformer-TokenMode,WORD +text-format-converter,firstLanguage,CONFIGURATION,CodeFormattingConverter-Language,YAML +text-format-converter,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-format-converter,secondLanguage,CONFIGURATION,CodeFormattingConverter-Language,XML +text-format-converter,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-format-converter,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-format-converter,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-format-converter,sourceFile,INPUT,kotlin.String,ZDlTmDpgSq0TAMFnAad2 +text-format-converter,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-format-converter,sourceText,INPUT,kotlin.String,hMTkiN2xzbLaIs8MMchW +text-format-converter,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-format-converter,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-format-converter,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-format-converter,targetFile,INPUT,kotlin.String,oDrkw1LAwPSvGpKEp2kz +text-format-converter,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-format-converter,targetText,INPUT,kotlin.String,hMTkiN2xzbLaIs8MMchW +text-sorting-transformer,caseInsensitive,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,liveConversion,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,removeBlankWords,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,removeDuplicates,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,reverseOrder,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,sortedIndividualJoinWordsDelimiter,CONFIGURATION,kotlin.String,JhmV03pKq3PV8vPafUgw +text-sorting-transformer,sortedJoinWordsDelimiter,CONFIGURATION,TextSortingTransformer-WordsDelimiter,SPACE +text-sorting-transformer,sortingOrder,CONFIGURATION,TextSortingTransformer-SortingOrder,WORD_LENGTH +text-sorting-transformer,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,sourceFile,INPUT,kotlin.String,vjUqEQrNfH58vgbzbZmt +text-sorting-transformer,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-sorting-transformer,sourceText,INPUT,kotlin.String,lPCnMmM4B0qzuKhBUJP1 +text-sorting-transformer,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-sorting-transformer,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,targetFile,INPUT,kotlin.String,HdbRXmpjT5FIuCmiUtku +text-sorting-transformer,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +text-sorting-transformer,trimWords,CONFIGURATION,kotlin.Boolean,false +text-sorting-transformer,unsortedIndividualSplitWordsDelimiter,CONFIGURATION,kotlin.String,UMqU3U6WmSolkyzgNkw0 +text-sorting-transformer,unsortedPredefinedDelimiter,CONFIGURATION,TextSortingTransformer-WordsDelimiter,SPACE +text-statistic,text,INPUT,kotlin.String,2yY0LvmRd2OPZGz9tf6U +text-statistic,text-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +text-statistic,text-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +text-statistic,text-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +ulid-generator,bulk-generation-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +ulid-generator,bulk-generation-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +ulid-generator,bulk-generation-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +ulid-generator,generateMonotonicUlid,CONFIGURATION,kotlin.Boolean,true +ulid-generator,individualTime,CONFIGURATION,kotlin.Long,1745745865516 +ulid-generator,ulidFormat,CONFIGURATION,UlidGenerator-UlidFormat,TO_LOWERCASE +ulid-generator,useIndividualTime,CONFIGURATION,kotlin.Boolean,true +unarchiver,archiveTreeSortingMode,CONFIGURATION,Unarchiver-SortingMode,FILENAME_DESC +unarchiver,clearTargetDirectory,CONFIGURATION,kotlin.Boolean,true +unarchiver,createArchiveFilenameSubDirectory,CONFIGURATION,kotlin.Boolean,false +unarchiver,createParentDirectories,CONFIGURATION,kotlin.Boolean,false +unarchiver,lastSelectedOpenedDirectoryPath,CONFIGURATION,kotlin.String,bMzWGRllBtNEjW0Gvh7D +unarchiver,lastSelectedTargetDirectoryPath,CONFIGURATION,kotlin.String,2cnrgGap8idZHDIRtkFE +unarchiver,openFileInEditorOnDoubleClick,CONFIGURATION,kotlin.Boolean,false +unarchiver,openTargetDirectoryAfterExtraction,CONFIGURATION,kotlin.Boolean,false +unarchiver,preserveDirectoryStructure,CONFIGURATION,kotlin.Boolean,false +unarchiver,preserveFileAttributes,CONFIGURATION,kotlin.Boolean,false +unarchiver,showArchiveNodeTotalNumberOfChildren,CONFIGURATION,kotlin.Boolean,false +unarchiver,showArchiveNodeUncompressedSize,CONFIGURATION,kotlin.Boolean,false +units-converter,baseConverter_baseTwoInput,INPUT,kotlin.String,00000110 +units-converter,baseConverter_showOnlyCommonBases,CONFIGURATION,kotlin.Boolean,false +units-converter,dataSizeConverter_bitDataSizeValue,INPUT,java.math.BigDecimal,1 +units-converter,dataSizeConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +units-converter,dataSizeConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer,rw-Latn-RW +units-converter,dataSizeConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,dataSizeConverter_roundingMode,CONFIGURATION,MathContextUnitConverter-RoundingMode,HALF_DOWN +units-converter,dataSizeConverter_showLargeDataUnits,CONFIGURATION,kotlin.Boolean,true +units-converter,lastSelectedUnitConverterIndex,CONFIGURATION,kotlin.Int,1 +units-converter,timeConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +units-converter,timeConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer,sk-Latn-SK +units-converter,timeConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,timeConverter_roundingMode,CONFIGURATION,MathContextUnitConverter-RoundingMode,HALF_DOWN +units-converter,timeConverter_timeNanoseconds,INPUT,java.math.BigDecimal,1 +units-converter,transferRateConverter_bitTransferRateValue,INPUT,java.math.BigDecimal,1 +units-converter,transferRateConverter_decimalPlaces,CONFIGURATION,kotlin.Int,6 +units-converter,transferRateConverter_parsingLocale,CONFIGURATION,dev.turingcomplete.intellijdevelopertoolsplugin.common.LocaleContainer,es-UY +units-converter,transferRateConverter_precision,CONFIGURATION,kotlin.Int,51 +units-converter,transferRateConverter_roundingMode,CONFIGURATION,MathContextUnitConverter-RoundingMode,HALF_DOWN +units-converter,transferRateConverter_showLargeDataUnits,CONFIGURATION,kotlin.Boolean,true +units-converter,transferRateConverter_timeDimension,CONFIGURATION,TransferRateConverter-TransferRateTimeDimension,MINUTES +units-converter,transferRateConverter_useCombinedAbbreviationNotation,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,sourceFile,INPUT,kotlin.String,ShXMbL5cuGRr3iGoD71W +url-base64-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +url-base64-encoder-decoder,sourceText,INPUT,kotlin.String,BFlkPjUlpMlKnls9kIP4 +url-base64-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-base64-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +url-base64-encoder-decoder,targetFile,INPUT,kotlin.String,5BYBfAOOIrJK80nR5Ryj +url-base64-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +url-base64-encoder-decoder,targetText,INPUT,kotlin.String,QkZsa1BqVWxwTWxLbmxzOWtJUDQ= +url-encoding-encoder-decoder,liveConversion,CONFIGURATION,kotlin.Boolean,false +url-encoding-encoder-decoder,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +url-encoding-encoder-decoder,sourceFile,INPUT,kotlin.String,7PrSPYQkhOCDfi8QcIMp +url-encoding-encoder-decoder,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +url-encoding-encoder-decoder,sourceText,INPUT,kotlin.String,QRVnPOIDwkGbbOg1ohq8 +url-encoding-encoder-decoder,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +url-encoding-encoder-decoder,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +url-encoding-encoder-decoder,targetFile,INPUT,kotlin.String,2mqJslQIX2ufro9Tx1va +url-encoding-encoder-decoder,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +url-encoding-encoder-decoder,targetText,INPUT,kotlin.String,QRVnPOIDwkGbbOg1ohq8 +uuid-generator,UUIDv1IndividualMacAddress,CONFIGURATION,kotlin.String,EUKARyRoCUxAMlSmmzeL +uuid-generator,UUIDv1LocalInterface,CONFIGURATION,kotlin.String,xJfYcWlauINi7TwQdQiQ +uuid-generator,UUIDv1MacAddressGenerationMode,CONFIGURATION,MacAddressBasedUuidGenerator-MacAddressGenerationMode,INDIVIDUAL +uuid-generator,UUIDv3IndividualNamespace,CONFIGURATION,kotlin.String,YoR04PtvXP9WF5jkQSST +uuid-generator,UUIDv3Name,CONFIGURATION,kotlin.String,x0EdmhlWfCRMhMyOPXui +uuid-generator,UUIDv3NamespaceMode,CONFIGURATION,NamespaceAndNameBasedUuidGenerator-NamespaceMode,INDIVIDUAL +uuid-generator,UUIDv3PredefinedNamespace,CONFIGURATION,NamespaceAndNameBasedUuidGenerator-PredefinedNamespace,URL +uuid-generator,UUIDv5IndividualNamespace,CONFIGURATION,kotlin.String,rMfzojcm8PdAIEdt8h2j +uuid-generator,UUIDv5Name,CONFIGURATION,kotlin.String,MfWptAVDSlb82J4AzRr6 +uuid-generator,UUIDv5NamespaceMode,CONFIGURATION,NamespaceAndNameBasedUuidGenerator-NamespaceMode,INDIVIDUAL +uuid-generator,UUIDv5PredefinedNamespace,CONFIGURATION,NamespaceAndNameBasedUuidGenerator-PredefinedNamespace,URL +uuid-generator,UUIDv6IndividualMacAddress,CONFIGURATION,kotlin.String,9EtwYiSbljyAV3U6c01F +uuid-generator,UUIDv6LocalInterface,CONFIGURATION,kotlin.String,W2dgBanOYzF2J5QMOjyU +uuid-generator,UUIDv6MacAddressGenerationMode,CONFIGURATION,MacAddressBasedUuidGenerator-MacAddressGenerationMode,INDIVIDUAL +uuid-generator,bulk-generation-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +uuid-generator,bulk-generation-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +uuid-generator,bulk-generation-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +uuid-generator,version,CONFIGURATION,UuidVersion,V5 +xml-text-escape,liveConversion,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,source-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,source-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,source-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,sourceFile,INPUT,kotlin.String,s003DGlWfHUZPbCgJsrQ +xml-text-escape,sourceFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +xml-text-escape,sourceText,INPUT,kotlin.String,VLGg7dLXEatCiwA0LwIj +xml-text-escape,target-editor-showSpecialCharacters,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,target-editor-showWhitespaces,CONFIGURATION,kotlin.Boolean,true +xml-text-escape,target-editor-softWraps,CONFIGURATION,kotlin.Boolean,false +xml-text-escape,targetFile,INPUT,kotlin.String,SfROIY0Wd40c3V53b6Eg +xml-text-escape,targetFileWriteFormat,CONFIGURATION,FileHandling-WriteFormat,HEX +xml-text-escape,targetText,INPUT,kotlin.String,VLGg7dLXEatCiwA0LwIj diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/instance-settings-persisted-state.xml b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/instance-settings-persisted-state.xml new file mode 100644 index 00000000..e511d12c --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/instance-settings-persisted-state.xml @@ -0,0 +1,704 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/removed-configuration-properties.csv b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/removed-configuration-properties.csv new file mode 100644 index 00000000..05e9d490 --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/removed-configuration-properties.csv @@ -0,0 +1,28 @@ +developerToolId,propertyKey +hmac-transformer,hmac-transformer-result-output-softWraps +hmac-transformer,hmac-transformer-result-output-showWhitespaces +hmac-transformer,hmac-transformer-result-output-showSpecialCharacters +text-filter,text-filter-result-output-softWraps +text-filter,text-filter-result-output-showSpecialCharacters +text-filter,text-filter-result-output-showWhitespaces +text-case-transformer,text-case-transformer-result-output-showWhitespaces +text-case-transformer,text-case-transformer-result-output-showSpecialCharacters +text-case-transformer,text-case-transformer-result-output-softWraps +sql-formatting,sql-formatting-result-output-showWhitespaces +sql-formatting,sql-formatting-result-output-softWraps +sql-formatting,sql-formatting-result-output-showSpecialCharacters +hashing-transformer,hashing-transformer-result-output-showSpecialCharacters +hashing-transformer,hashing-transformer-result-output-showWhitespaces +hashing-transformer,hashing-transformer-result-output-softWraps +line-breaks-encoder-decoder,line-breaks-encoder-decoder-target-softWraps +line-breaks-encoder-decoder,liveConversion +line-breaks-encoder-decoder,lineBreakDecoding +line-breaks-encoder-decoder,line-breaks-encoder-decoder-source-showWhitespaces +line-breaks-encoder-decoder,line-breaks-encoder-decoder-target-showSpecialCharacters +line-breaks-encoder-decoder,sourceText +line-breaks-encoder-decoder,line-breaks-encoder-decoder-source-softWraps +line-breaks-encoder-decoder,line-breaks-encoder-decoder-target-showWhitespaces +line-breaks-encoder-decoder,line-breaks-encoder-decoder-source-showSpecialCharacters +text-sorting-transformer,text-sorting-transformer-result-output-showWhitespaces +text-sorting-transformer,text-sorting-transformer-result-output-showSpecialCharacters +text-sorting-transformer,text-sorting-transformer-result-output-softWraps diff --git a/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/renamed-configuration-properties.csv b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/renamed-configuration-properties.csv new file mode 100644 index 00000000..43b85f54 --- /dev/null +++ b/src/test/resources/dev/turingcomplete/intellijdevelopertoolsplugin/integrationtest/instancesettings/7.0.0/renamed-configuration-properties.csv @@ -0,0 +1,166 @@ +developerToolId,oldPropertyKey,newPropertyKey +text-statistic,text-statistic-text-showWhitespaces,text-editor-showWhitespaces +text-statistic,text-statistic-text-showSpecialCharacters,text-editor-showSpecialCharacters +text-statistic,text-statistic-text-softWraps,text-editor-softWraps +notes,notes-content-showSpecialCharacters,content-editor-showSpecialCharacters +notes,notes-content-softWraps,content-editor-softWraps +notes,notes-content-showWhitespaces,content-editor-showWhitespaces +password-generator,password-generator-bulk-generation-softWraps,bulk-generation-editor-softWraps +password-generator,password-generator-bulk-generation-showWhitespaces,bulk-generation-editor-showWhitespaces +password-generator,password-generator-bulk-generation-showSpecialCharacters,bulk-generation-editor-showSpecialCharacters +json-schema-validator,json-schema-validator-schema-showSpecialCharacters,schema-editor-showSpecialCharacters +json-schema-validator,json-schema-validator-data-showSpecialCharacters,data-editor-showSpecialCharacters +json-schema-validator,json-schema-validator-data-showWhitespaces,data-editor-showWhitespaces +json-schema-validator,json-schema-validator-schema-softWraps,schema-editor-softWraps +json-schema-validator,json-schema-validator-data-softWraps,data-editor-softWraps +json-schema-validator,json-schema-validator-schema-showWhitespaces,schema-editor-showWhitespaces +base64-encoder-decoder,base64-encoder-decoder-source-softWraps,source-editor-softWraps +base64-encoder-decoder,base64-encoder-decoder-source-showSpecialCharacters,source-editor-showSpecialCharacters +base64-encoder-decoder,base64-encoder-decoder-target-softWraps,target-editor-softWraps +base64-encoder-decoder,base64-encoder-decoder-source-showWhitespaces,source-editor-showWhitespaces +base64-encoder-decoder,base64-encoder-decoder-target-showWhitespaces,target-editor-showWhitespaces +base64-encoder-decoder,base64-encoder-decoder-target-showSpecialCharacters,target-editor-showSpecialCharacters +json-text-escape,json-text-escape-source-softWraps,source-editor-softWraps +json-text-escape,json-text-escape-target-showWhitespaces,target-editor-showWhitespaces +json-text-escape,json-text-escape-source-showSpecialCharacters,source-editor-showSpecialCharacters +json-text-escape,json-text-escape-target-softWraps,target-editor-softWraps +json-text-escape,json-text-escape-target-showSpecialCharacters,target-editor-showSpecialCharacters +json-text-escape,json-text-escape-source-showWhitespaces,source-editor-showWhitespaces +ascii-art,ascii-art-asciiArtOutput-softWraps,asciiArtOutput-editor-softWraps +ascii-art,ascii-art-asciiArtOutput-showWhitespaces,asciiArtOutput-editor-showWhitespaces +ascii-art,ascii-art-asciiArtOutput-showSpecialCharacters,asciiArtOutput-editor-showSpecialCharacters +url-encoding-encoder-decoder,url-encoding-encoder-decoder-target-showSpecialCharacters,target-editor-showSpecialCharacters +url-encoding-encoder-decoder,url-encoding-encoder-decoder-source-softWraps,source-editor-softWraps +url-encoding-encoder-decoder,url-encoding-encoder-decoder-source-showSpecialCharacters,source-editor-showSpecialCharacters +url-encoding-encoder-decoder,url-encoding-encoder-decoder-source-showWhitespaces,source-editor-showWhitespaces +url-encoding-encoder-decoder,url-encoding-encoder-decoder-target-showWhitespaces,target-editor-showWhitespaces +url-encoding-encoder-decoder,url-encoding-encoder-decoder-target-softWraps,target-editor-softWraps +cli-command-converter,cli-command-converter-target-showSpecialCharacters,target-editor-showSpecialCharacters +cli-command-converter,cli-command-converter-source-showSpecialCharacters,source-editor-showSpecialCharacters +cli-command-converter,cli-command-converter-source-softWraps,source-editor-softWraps +cli-command-converter,cli-command-converter-target-softWraps,target-editor-softWraps +cli-command-converter,cli-command-converter-source-showWhitespaces,source-editor-showWhitespaces +cli-command-converter,cli-command-converter-target-showWhitespaces,target-editor-showWhitespaces +ascii-encoder-decoder,ascii-encoder-decoder-source-showSpecialCharacters,source-editor-showSpecialCharacters +ascii-encoder-decoder,ascii-encoder-decoder-target-softWraps,target-editor-softWraps +ascii-encoder-decoder,ascii-encoder-decoder-target-showWhitespaces,target-editor-showWhitespaces +ascii-encoder-decoder,ascii-encoder-decoder-target-showSpecialCharacters,target-editor-showSpecialCharacters +ascii-encoder-decoder,ascii-encoder-decoder-source-softWraps,source-editor-softWraps +ascii-encoder-decoder,ascii-encoder-decoder-source-showWhitespaces,source-editor-showWhitespaces +html-entities-escape,html-entities-escape-target-softWraps,target-editor-softWraps +html-entities-escape,html-entities-escape-source-showSpecialCharacters,source-editor-showSpecialCharacters +html-entities-escape,html-entities-escape-source-showWhitespaces,source-editor-showWhitespaces +html-entities-escape,html-entities-escape-source-softWraps,source-editor-softWraps +html-entities-escape,html-entities-escape-target-showWhitespaces,target-editor-showWhitespaces +html-entities-escape,html-entities-escape-target-showSpecialCharacters,target-editor-showSpecialCharacters +java-text-escape,java-text-escape-source-showSpecialCharacters,source-editor-showSpecialCharacters +java-text-escape,java-text-escape-source-softWraps,source-editor-softWraps +java-text-escape,java-text-escape-target-showSpecialCharacters,target-editor-showSpecialCharacters +java-text-escape,java-text-escape-source-showWhitespaces,source-editor-showWhitespaces +java-text-escape,java-text-escape-target-softWraps,target-editor-softWraps +java-text-escape,java-text-escape-target-showWhitespaces,target-editor-showWhitespaces +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-encoded-showWhitespaces,encoded-editor-showWhitespaces +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-header-showSpecialCharacters,header-editor-showSpecialCharacters +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-payload-showWhitespaces,payload-editor-showWhitespaces +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-payload-showSpecialCharacters,payload-editor-showSpecialCharacters +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-header-softWraps,header-editor-softWraps +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-encoded-softWraps,encoded-editor-softWraps +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-payload-softWraps,payload-editor-softWraps +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-encoded-showSpecialCharacters,encoded-editor-showSpecialCharacters +jwt-encoder-decoder,jwt-encoder-decoder-jwt-encoder-decoder-header-showWhitespaces,header-editor-showWhitespaces +hmac-transformer,hmac-transformer-source-input-showWhitespaces,target-editor-showWhitespaces +hmac-transformer,hmac-transformer-source-input-softWraps,target-editor-softWraps +hmac-transformer,liveTransformation,liveConversion +hmac-transformer,hmac-transformer-source-input-showSpecialCharacters,target-editor-showSpecialCharacters +json-path,json-path-source-input-softWraps,source-input-editor-softWraps +json-path,json-path-result-output-softWraps,result-output-editor-softWraps +json-path,json-path-result-output-showWhitespaces,result-output-editor-showWhitespaces +json-path,json-path-source-input-showSpecialCharacters,source-input-editor-showSpecialCharacters +json-path,json-path-source-input-showWhitespaces,source-input-editor-showWhitespaces +json-path,json-path-result-output-showSpecialCharacters,result-output-editor-showSpecialCharacters +code-style-formatting,code-style-formatting-source-input-softWraps,source-input-editor-softWraps +code-style-formatting,code-style-formatting-result-output-softWraps,result-output-editor-softWraps +code-style-formatting,code-style-formatting-result-output-showSpecialCharacters,result-output-editor-showSpecialCharacters +code-style-formatting,code-style-formatting-result-output-showWhitespaces,result-output-editor-showWhitespaces +code-style-formatting,code-style-formatting-source-input-showSpecialCharacters,source-input-editor-showSpecialCharacters +code-style-formatting,code-style-formatting-source-input-showWhitespaces,source-input-editor-showWhitespaces +text-filter,text-filter-source-input-softWraps,target-editor-softWraps +text-filter,liveTransformation,liveConversion +text-filter,text-filter-source-input-showWhitespaces,target-editor-showWhitespaces +text-filter,text-filter-source-input-showSpecialCharacters,target-editor-showSpecialCharacters +ulid-generator,ulid-generator-bulk-generation-softWraps,bulk-generation-editor-softWraps +ulid-generator,ulid-generator-bulk-generation-showSpecialCharacters,bulk-generation-editor-showSpecialCharacters +ulid-generator,ulid-generator-bulk-generation-showWhitespaces,bulk-generation-editor-showWhitespaces +base32-encoder-decoder,base32-encoder-decoder-source-showWhitespaces,source-editor-showWhitespaces +base32-encoder-decoder,base32-encoder-decoder-target-showWhitespaces,target-editor-showWhitespaces +base32-encoder-decoder,base32-encoder-decoder-target-softWraps,target-editor-softWraps +base32-encoder-decoder,base32-encoder-decoder-source-softWraps,source-editor-softWraps +base32-encoder-decoder,base32-encoder-decoder-source-showSpecialCharacters,source-editor-showSpecialCharacters +base32-encoder-decoder,base32-encoder-decoder-target-showSpecialCharacters,target-editor-showSpecialCharacters +text-format-converter,text-format-converter-source-showSpecialCharacters,source-editor-showSpecialCharacters +text-format-converter,text-format-converter-source-softWraps,source-editor-softWraps +text-format-converter,text-format-converter-target-showSpecialCharacters,target-editor-showSpecialCharacters +text-format-converter,text-format-converter-target-showWhitespaces,target-editor-showWhitespaces +text-format-converter,text-format-converter-source-showWhitespaces,source-editor-showWhitespaces +text-format-converter,text-format-converter-target-softWraps,target-editor-softWraps +qr-code-generator,qr-code-generator-content-softWraps,content-editor-softWraps +qr-code-generator,qr-code-generator-content-showWhitespaces,content-editor-showWhitespaces +qr-code-generator,qr-code-generator-content-showSpecialCharacters,content-editor-showSpecialCharacters +nano-id-generator,nano-id-generator-bulk-generation-showWhitespaces,bulk-generation-editor-showWhitespaces +nano-id-generator,nano-id-generator-bulk-generation-softWraps,bulk-generation-editor-softWraps +nano-id-generator,nano-id-generator-bulk-generation-showSpecialCharacters,bulk-generation-editor-showSpecialCharacters +mime-base64-encoder-decoder,mime-base64-encoder-decoder-source-showSpecialCharacters,source-editor-showSpecialCharacters +mime-base64-encoder-decoder,mime-base64-encoder-decoder-source-showWhitespaces,source-editor-showWhitespaces +mime-base64-encoder-decoder,mime-base64-encoder-decoder-target-softWraps,target-editor-softWraps +mime-base64-encoder-decoder,mime-base64-encoder-decoder-target-showWhitespaces,target-editor-showWhitespaces +mime-base64-encoder-decoder,mime-base64-encoder-decoder-target-showSpecialCharacters,target-editor-showSpecialCharacters +mime-base64-encoder-decoder,mime-base64-encoder-decoder-source-softWraps,source-editor-softWraps +regular-expression-matcher,regular-expression-matcher-extraction-result-showSpecialCharacters,extraction-result-editor-showSpecialCharacters +regular-expression-matcher,regular-expression-matcher-input-showWhitespaces,input-editor-showWhitespaces +regular-expression-matcher,regular-expression-matcher-substitution-result-showWhitespaces,substitution-result-editor-showWhitespaces +regular-expression-matcher,regular-expression-matcher-substitution-result-showSpecialCharacters,substitution-result-editor-showSpecialCharacters +regular-expression-matcher,regular-expression-matcher-extraction-result-showWhitespaces,extraction-result-editor-showWhitespaces +regular-expression-matcher,regular-expression-matcher-extraction-result-softWraps,extraction-result-editor-softWraps +regular-expression-matcher,regular-expression-matcher-input-softWraps,input-editor-softWraps +regular-expression-matcher,regular-expression-matcher-input-showSpecialCharacters,input-editor-showSpecialCharacters +regular-expression-matcher,regular-expression-matcher-substitution-result-softWraps,substitution-result-editor-softWraps +uuid-generator,uuid-generator-bulk-generation-showWhitespaces,bulk-generation-editor-showWhitespaces +uuid-generator,uuid-generator-bulk-generation-softWraps,bulk-generation-editor-softWraps +uuid-generator,uuid-generator-bulk-generation-showSpecialCharacters,bulk-generation-editor-showSpecialCharacters +text-case-transformer,liveTransformation,liveConversion +text-case-transformer,text-case-transformer-source-input-showSpecialCharacters,target-editor-showSpecialCharacters +text-case-transformer,text-case-transformer-source-input-showWhitespaces,target-editor-showWhitespaces +text-case-transformer,text-case-transformer-source-input-softWraps,target-editor-softWraps +lorem-ipsum-generator,lorem-ipsum-generator-generated-text-showSpecialCharacters,generated-text-editor-showSpecialCharacters +lorem-ipsum-generator,lorem-ipsum-generator-generated-text-showWhitespaces,generated-text-editor-showWhitespaces +lorem-ipsum-generator,lorem-ipsum-generator-generated-text-softWraps,generated-text-editor-softWraps +url-base64-encoder-decoder,url-base64-encoder-decoder-source-showSpecialCharacters,source-editor-showSpecialCharacters +url-base64-encoder-decoder,url-base64-encoder-decoder-source-softWraps,source-editor-softWraps +url-base64-encoder-decoder,url-base64-encoder-decoder-source-showWhitespaces,source-editor-showWhitespaces +url-base64-encoder-decoder,url-base64-encoder-decoder-target-showSpecialCharacters,target-editor-showSpecialCharacters +url-base64-encoder-decoder,url-base64-encoder-decoder-target-showWhitespaces,target-editor-showWhitespaces +url-base64-encoder-decoder,url-base64-encoder-decoder-target-softWraps,target-editor-softWraps +csv-text-escape,csv-text-escape-target-showSpecialCharacters,target-editor-showSpecialCharacters +csv-text-escape,csv-text-escape-target-showWhitespaces,target-editor-showWhitespaces +csv-text-escape,csv-text-escape-source-showWhitespaces,source-editor-showWhitespaces +csv-text-escape,csv-text-escape-target-softWraps,target-editor-softWraps +csv-text-escape,csv-text-escape-source-softWraps,source-editor-softWraps +csv-text-escape,csv-text-escape-source-showSpecialCharacters,source-editor-showSpecialCharacters +sql-formatting,sql-formatting-source-input-showWhitespaces,target-editor-showWhitespaces +sql-formatting,liveTransformation,liveConversion +sql-formatting,sql-formatting-source-input-showSpecialCharacters,target-editor-showSpecialCharacters +sql-formatting,sql-formatting-source-input-softWraps,target-editor-softWraps +hashing-transformer,hashing-transformer-source-input-showWhitespaces,target-editor-showWhitespaces +hashing-transformer,liveTransformation,liveConversion +hashing-transformer,hashing-transformer-source-input-showSpecialCharacters,target-editor-showSpecialCharacters +hashing-transformer,hashing-transformer-source-input-softWraps,target-editor-softWraps +xml-text-escape,xml-text-escape-source-softWraps,source-editor-softWraps +xml-text-escape,xml-text-escape-source-showSpecialCharacters,source-editor-showSpecialCharacters +xml-text-escape,xml-text-escape-source-showWhitespaces,source-editor-showWhitespaces +xml-text-escape,xml-text-escape-target-softWraps,target-editor-softWraps +xml-text-escape,xml-text-escape-target-showWhitespaces,target-editor-showWhitespaces +xml-text-escape,xml-text-escape-target-showSpecialCharacters,target-editor-showSpecialCharacters +text-sorting-transformer,liveTransformation,liveConversion +text-sorting-transformer,text-sorting-transformer-source-input-showSpecialCharacters,target-editor-showSpecialCharacters +text-sorting-transformer,text-sorting-transformer-source-input-softWraps,target-editor-softWraps +text-sorting-transformer,text-sorting-transformer-source-input-showWhitespaces,target-editor-showWhitespaces