diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a717ab9f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,1211 @@ +# import package 规则顺序参考的是: https://github.com/spring-projects/spring-framework/wiki/Code-Style#import-statements +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +trim_trailing_whitespace = true +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = true +ij_smart_tabs = false +ij_visual_guides = +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +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 = false +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 = false +ij_java_align_multiline_throws_list = false +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 = +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 = false +ij_java_class_annotation_wrap = split_into_lines +ij_java_class_brace_style = end_of_line +ij_java_class_count_to_use_import_on_demand = 99 +ij_java_class_names_in_javadoc = 1 +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 = always +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 = false +ij_java_entity_dd_prefix = +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_prefix = +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_prefix = +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_li_suffix = +ij_java_entity_pk_class = java.lang.String +ij_java_entity_ri_prefix = +ij_java_entity_ri_suffix = +ij_java_entity_vo_prefix = +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +ij_java_field_name_prefix = +ij_java_field_name_suffix = +ij_java_filter_class_prefix = +ij_java_filter_class_suffix = +ij_java_filter_dd_prefix = +ij_java_filter_dd_suffix = +ij_java_finally_on_new_line = false +ij_java_for_brace_force = always +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 = always +ij_java_imports_layout = java.**,|,javax.**,jakarta.**,|,*,|,org.springframework.**,|,$* +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_listener_class_prefix = +ij_java_listener_class_suffix = +ij_java_local_variable_name_prefix = +ij_java_local_variable_name_suffix = +ij_java_message_dd_prefix = +ij_java_message_dd_suffix = EJB +ij_java_message_eb_prefix = +ij_java_message_eb_suffix = Bean +ij_java_method_annotation_wrap = split_into_lines +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 = 99 +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_parameter_name_prefix = +ij_java_parameter_name_suffix = +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 = normal +ij_java_repeat_annotations = +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_servlet_class_prefix = +ij_java_servlet_class_suffix = +ij_java_servlet_dd_prefix = +ij_java_servlet_dd_suffix = +ij_java_session_dd_prefix = +ij_java_session_dd_suffix = EJB +ij_java_session_eb_prefix = +ij_java_session_eb_suffix = Bean +ij_java_session_hi_prefix = +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_li_suffix = +ij_java_session_ri_prefix = +ij_java_session_ri_suffix = +ij_java_session_si_prefix = +ij_java_session_si_suffix = Service +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_static_field_name_prefix = +ij_java_static_field_name_suffix = +ij_java_subclass_name_prefix = +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_prefix = +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 = always +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.proto] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_protobuf_keep_blank_lines_in_code = 2 +ij_protobuf_keep_indents_on_empty_lines = false +ij_protobuf_keep_line_breaks = true +ij_protobuf_space_after_comma = true +ij_protobuf_space_before_comma = false +ij_protobuf_spaces_around_assignment_operators = true +ij_protobuf_spaces_within_braces = false +ij_protobuf_spaces_within_brackets = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.vue] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_vue_indent_children_of_top_level = template +ij_vue_interpolation_new_line_after_start_delimiter = true +ij_vue_interpolation_new_line_before_end_delimiter = true +ij_vue_interpolation_wrap = off +ij_vue_keep_indents_on_empty_lines = false +ij_vue_spaces_within_interpolation_expressions = true + +[.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,*.pom,*.rng,*.tld,*.wadl,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = off +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 + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = always +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = always +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = always +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_object_types_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_property_prefix = +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_import_type = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = always +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +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 + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx,node_modules/**,**/node_modules/**,@angular/material,@angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = always +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = always +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = always +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_object_types_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_property_prefix = +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_import_type = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = always +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.ft,*.vm,*.vsl}] +ij_vtl_keep_indents_on_empty_lines = false + +[{*.gant,*.groovy,*.gy}] +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 = always +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 = always +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 = always +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 = always +ij_groovy_while_on_new_line = false +ij_groovy_wrap_chain_calls_after_dot = false +ij_groovy_wrap_long_lines = false + +[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +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,*.ng,*.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 + +[{*.http,*.rest}] +indent_size = 0 +ij_continuation_indent_size = 4 +ij_http-request_call_parameters_wrap = normal +ij_http-request_method_parameters_wrap = split_into_lines +ij_http-request_space_before_comma = true +ij_http-request_spaces_around_assignment_operators = true + +[{*.jsf,*.jsp,*.jspf,*.tag,*.tagf,*.xjsp}] +ij_jsp_jsp_prefer_comma_separated_import_list = false +ij_jsp_keep_indents_on_empty_lines = false + +[{*.jspx,*.tagx}] +ij_jspx_keep_indents_on_empty_lines = false + +[{*.kt,*.kts}] +ij_kotlin_align_in_columns_case_branch = false +ij_kotlin_align_multiline_binary_operation = false +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 = false +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_assignment_wrap = off +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_right_paren_on_new_line = false +ij_kotlin_call_parameters_wrap = off +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +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 = true +ij_kotlin_continuation_indent_in_if_conditions = true +ij_kotlin_continuation_indent_in_parameter_lists = true +ij_kotlin_continuation_indent_in_supertype_lists = true +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = off +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 = 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 = split_into_lines +ij_kotlin_method_call_chain_wrap = off +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 = 5 +ij_kotlin_name_count_to_use_star_import_for_members = 3 +ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.** +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_when_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_range = false +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 = false +ij_kotlin_wrap_elvis_expressions = 1 +ij_kotlin_wrap_expression_body_functions = 0 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.markdown,*.md}] +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 + +[{*.pb,*.textproto,*.txtpb}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +ij_prototext_keep_blank_lines_in_code = 2 +ij_prototext_keep_indents_on_empty_lines = false +ij_prototext_keep_line_breaks = true +ij_prototext_space_after_colon = true +ij_prototext_space_after_comma = true +ij_prototext_space_before_colon = false +ij_prototext_space_before_comma = false +ij_prototext_spaces_within_braces = true +ij_prototext_spaces_within_brackets = false + +[{*.properties,spring.handlers,spring.schemas}] +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 = true + +[{*.qute.htm,*.qute.html,*.qute.json,*.qute.txt,*.qute.yaml,*.qute.yml}] +ij_qute_keep_indents_on_empty_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +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 diff --git a/.github/workflows/sync-to-gitee.yml b/.github/workflows/sync-to-gitee.yml deleted file mode 100644 index aee15b21..00000000 --- a/.github/workflows/sync-to-gitee.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Sync to Gitee -run-name: "[${{ github.workflow }}] - ${{ github.event.head_commit.message }}" - -on: - push: - branches: - - main - -concurrency: - group: ${{ github.workflow}} - cancel-in-progress: true - -env: - GITEE_REPOSITORY: zaonline/devpilot-intellij -jobs: - sync_to_gitee: - runs-on: ubuntu-latest - if: github.repository == 'openpilot-hub/devpilot-intellij' - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - run: | - remote_repo="https://${{ secrets.GITEE_USERNAME }}:${{ secrets.GITEE_PAT }}@gitee.com/${{ env.GITEE_REPOSITORY }}.git" - git remote add gitee "${remote_repo}" - branch=$(git branch --show-current) - git push gitee $branch -f diff --git a/.gitignore b/.gitignore index ac03c439..3381e8b4 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ bin/ .DS_Store .idea + +src/main/resources/config/local.properties \ No newline at end of file diff --git a/BUILD_PLUGIN.md b/BUILD_PLUGIN.md new file mode 100644 index 00000000..aaa4b878 --- /dev/null +++ b/BUILD_PLUGIN.md @@ -0,0 +1,6 @@ +# Prerequisite + +If you want to build your own complete plugin like DevPilot, there are some required condition: +1. AI gateway: support multi LLM model and provide api for plugin [Gateway repo](https://github.com/openpilot-hub/devpilot-gateway) +2. Auth System: support authorization check for login user (You can close it by setting `DefaultConst.AUTH_ON` to false) +3. Telemetry System: upload user behavior data for analysis (You can close it by setting `DefaultConst.TELEMETRY_ON` to false) \ No newline at end of file diff --git a/BUILD_PLUGIN_ZH.md b/BUILD_PLUGIN_ZH.md new file mode 100644 index 00000000..d9823fe7 --- /dev/null +++ b/BUILD_PLUGIN_ZH.md @@ -0,0 +1,6 @@ +# 前提条件 + +如果想构建一个完整的属于你自己的DevPilot应用,需要有如下几个条件: +1. AI网关:用于兼容不同的LLM模型,并提供API给插件使用 [网关仓库](https://github.com/openpilot-hub/devpilot-gateway) +2. 权限系统:用于校验插件用户的登录和使用权限(可以通过设置`DefaultConst.AUTH_ON`为false来关闭) +3. 指标系统:用于处理用户上报的使用数据用于分析(可以通过设置`DefaultConst.TELEMETRY_ON`为false来关闭) \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c4f447c..bb43564f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,15 +11,19 @@ Java 11 or higher version is required to build and test this repository. ### Building 1. Clone this repository to your local machine. - `git clone https://github.com/openpilot-hub/devpilot-intellij.git` 2. Run the following command to build the project. - `./gradlew runIde` 3. If you are using windows, run the following command to build the project. - `gradlew.bat runIde` +### Update Webview + +1. Clone this repository + `https://github.com/openpilot-hub/devpilot-h5` +2. Run `pnpm install` and `pnpm run build` +3. `cp dist/ext.html ../devpilot-intellij/src/main/resources/webview/index.html` + ### Testing & Checks Before you commit your changes, please run the following command to check if there are any errors. diff --git a/CONTRIBUTING_ZH.md b/CONTRIBUTING_ZH.md index c656d7c6..5a11d5b4 100644 --- a/CONTRIBUTING_ZH.md +++ b/CONTRIBUTING_ZH.md @@ -11,15 +11,19 @@ ### 项目构建 1. 将项目克隆到本地。 - `git clone https://github.com/openpilot-hub/devpilot-intellij.git` 2. 运行如下指令构建项目。 - `./gradlew runIde` 3. 如果你使用的是 windows,运行如下指令构建项目。 - `gradlew.bat runIde` +### 更新 Webview 页面 + +1. 克隆前端webview项目 + `https://github.com/openpilot-hub/devpilot-h5` +2. 执行 `pnpm install` 和 `pnpm run build` +3. 执行`cp dist/ext.html ../devpilot-intellij/src/main/resources/webview/index.html` + ### 测试和检查 在提交commit之前,请运行如下指令检查是否有错误。 diff --git a/README.md b/README.md index c8743d15..dc0b6be7 100644 --- a/README.md +++ b/README.md @@ -17,155 +17,17 @@ This innovative AI-based plugin is set to supercharge your development process. 5. **Code Explanation:** Don't just write code, understand it! DevPilot can explain unfamiliar code snippets, helping you grasp what's happening and learn faster. 6. **Auto-Comments:** Keep your code clear! DevPilot can automatically add comments to your code, ensuring it's easy to understand and maintain. -## Prerequisites - -1. You will need an OpenAI API key / Codellama API endpoint before you use this plugin. -2. For use of Codellama, see [deploy Codellama into you local environment](https://github.com/openpilot-hub/codellama-deploy). - -## Installation - -Currently, the plugin has version requirements for IntelliJ IDEA. The minimum required version is 2021.2, make sure you have the appropriate version installed before attempting to use the plugin. - -There are three ways for installing plugins: - -- IDEA official marketplace - You can get the plugin from [here](https://plugins.jetbrains.com/plugin/23322-devpilot). - -- Installation via compressed package - select "install from disk" in the plugin page settings, and then import the zip package. The download link for the compressed package is available in [Release page](https://github.com/openpilot-hub/devpilot-intellij/releases). - ![img.png](doc/images/screenshot/cn/install_from_disk.png) -- Build from scratch - 1. Clone this repository to your local machine. - `git clone https://github.com/openpilot-hub/devpilot-intellij.git` - 2. Run the following command to build the project. - - `./gradlew runIde` - - If you are using windows, run the following command to build the project. - - `gradlew.bat runIde` - -## Configuration - -After the successful installation of the plugin, you can find the configuration page of DevPilot in the Tools section of the IDEA settings page. -open the settings with command+,, - -then navigate to Tools ❯ DevPilot ❯ Service Configuration. - -![configuration](doc/images/screenshot/en/config.png) - -On the configuration page, you can switch the language of the plugin between multiple choices, as well as select different models and their addresses. The username will be displayed on the page during the conversation. -![settings](doc/images/screenshot/en/settings.png) - ## How to use -In the dialog box, you can ask any question and send it by pressing Enter or using the send button. DevPilot will then provide an answer and display it on the current page. - -Select a block of code, then right-click on the "DevPilot" option on the context menu.You can choose from one of: - -- New DevPilot Chat -- Generate Tests -- Review Code -- Generate Comments -- Fix This -- Performance Check -- Explain This - -![menu](doc/images/screenshot/en/chat_menu.png) - -**For example**, select the code, right-click ❯ DevPilot ❯ Fix This. - -Then, the window will automatically open and provide relevant suggestions for fixes. You can modify the code based on the suggestions. - -## Plugin Functionality - -DevPilot provides developers with rich capabilities to help them eliminate the tedious work of writing test cases and comments, write more efficient code, and troubleshoot code issues, among other things. - -### Chat with DevPilot - -The plugin supports chat, the conversation page can be opened through the right-click menu or the entry on the right sidebar. - -![menu chat](doc/images/screenshot/en/menu_chat.png) - -You can write questions in the dialogue box, send them by pressing enter or the send button, and OpenAI will provide answers and display them on the current page. - -![welcome](doc/images/screenshot/en/welcome.png) - -### Generate test cases - -Users can generate test cases through our plugin. Users can right-click on a selected method and click "Generate Tests". - -![menu testcase](doc/images/screenshot/en/menu_testcase.png) - -Afterward, a window will automatically open and the window will return the relevant test cases. Users can directly copy the test or choose to insert or replace the relevant code at the cursor position. They can even create a new test class file. - -![testcase result](doc/images/screenshot/en/testcase_result.png) - -### Generate comments - -Plugin supports the function of generating code comments. Users can right-click and select "Generate comments" after selecting a code block. - -![menu gen comments](doc/images/screenshot/en/menu_gen_comments.png) - -Afterward, the window will automatically open and return the relevant annotation results. The edited code will also automatically show the generated annotations and the original code in a diff format, allowing users to compare and accept the corresponding annotations. - -### Fix bugs - -The plugin supports the ability to fix code. Users can right-click on "Fix This" after selecting a code block. - -![menu fix bug](doc/images/screenshot/en/menu_fix_bug.png) - -Afterward, the window will automatically open and provide relevant repair suggestions. Users can modify their code based on the suggestions. - -![fix bugs result](doc/images/screenshot/en/fix_bug_result.png) - -### Review code - -The plugin supports the ability to review code. Users can right-click on the selected code block and click "Review Code". - -![menu code review.png](doc/images/screenshot/en/menu_code_review.png) - -Afterward, the window will automatically open and return the relevant review results. Users can edit their code logic based on the results. - -![review code result](doc/images/screenshot/en/code_review_result.png) - -### Performance Check - -The plugin supports performance checking of code. Users can right-click on the selected code block and choose "Performance Check". - -![menu performance check](doc/images/screenshot/en/menu_performance_check.png) - -Afterward, the window will automatically open and return the relevant performance test results. Additionally, the optimized code and the original code will be automatically compared using diff in the editor, allowing users to optimize their code based on the results. - -![performance check result](doc/images/screenshot/en/performance_check_result.png) - -### Clear context - -The overall capabilities of the plugin are context-dependent, but the context may be polluted, and OpenAI or other large models may have token limitations that result in errors when exceeded. Therefore, we provide the ability to clean up by clicking the clear button in the upper right corner of the window, which will clear the entire session. - -![menu clear context](doc/images/screenshot/en/clear_context.png) - -## Roadmap - -At DevPilot, we are constantly working on expanding the capabilities of our AI-powered plugin to meet the evolving needs of our users. Our roadmap is a reflection of our commitment to continually improve and innovate. - -### Near-Term Goals : integration with local LLM - -We weill soon support Codellama, an opensource LLM developed by Meta, into DevPilot. This will provide you with the ability to leverage the power of LLM directly in your local development environment, without sending any sensitive data to OpenAI. - -With Codellama integration, users can expect: - -- More accurate and context-specific code suggestions, Codellama has a 100k context length, so the suggestions it provides will be based on a deeper understanding of the code's context. This will significantly reduce the time developers spend on figuring out the appropriate code to use. -- With its fine-tuning on a large codebase, Codellama can outperform ChatGPT in certain cases. +[DevPilot for JetBrains](https://github.com/openpilot-hub/documentation/blob/main/README_JetBrains_EN.md) -### Long-Term Goals: Model as a Service (MaaS) Support +## RAG -Looking ahead, we plan to extend our capabilities further by supporting Model as a Service (MaaS). MaaS provides the ability for users to utilize AI models in a service-based manner, without the need for local deployment or maintenance. This feature will enable seamless access to the latest AI models without bearing the computational costs or complexities of managing these models locally. +[DevPilot RAG](https://github.com/openpilot-hub/documentation/blob/main/README_RAG_EN.md) -By integrating MaaS into DevPilot, we aim to: +## Build your own plugin -- Access the latest OpenAI models without the hassle of setting up a OpenAI API key. -- Provide a plug-and-play solution for accessing state-of-the-art AI models -- Allow developers to leverage the latest AI advancements without the need for local resources. -- Offer a scalable solution that can grow with your project's needs. +[Build DevPilot](BUILD_PLUGIN.md) ## Contributing diff --git a/README_ZH.md b/README_ZH.md index d0c5bdc3..d585ad4f 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -4,138 +4,39 @@ ## 立即体验 -使用DevPilot,这个为IntelliJ IDEA专门设计的新型编程伙伴,释放AI在您编程中的强大力量。 +使用DevPilot,这个为IntelliJ IDEA专门设计的新型编程伙伴,释放AI在您编程中的强大力量。 这个创新的基于AI的插件将会提升您的开发流程。 ## 卓越特性 -1. **智能代码建议:** 结束编程难点! DevPilot 在你编辑时实时提供代码建议,理解您的上下文并给出精准的建议。 +1. **智能代码建议:** 结束编程难点! DevPilot 在你编辑时实时提供代码建议,理解您的上下文并给出精准的建议。 -2. **主动错误检测:** 避免错误! DevPilot 在错误出现前发现潜在的bug和错误,提供明智的解决方案和替代方法来编写高效、无错误的代码。 +2. **主动错误检测:** 避免错误! DevPilot 在错误出现前发现潜在的bug和错误,提供明智的解决方案和替代方法来编写高效、无错误的代码。 -3. **代码重构:** 提升您的代码! DevPilot 帮助优化代码,提供见解告诉您如何重构和提高代码的结构和性能。 +3. **代码重构:** 提升您的代码! DevPilot 帮助优化代码,提供见解告诉您如何重构和提高代码的结构和性能。 -4. **单元测试生成:** 测试变简单! DevPilot 可以为您生成单元测试代码,确保您的代码不仅可以按预期工作,而且也准备好应对任何未来的更改。 +4. **单元测试生成:** 测试变简单! DevPilot 可以为您生成单元测试代码,确保您的代码不仅可以按预期工作,而且也准备好应对任何未来的更改。 -5. **代码解释:** 不仅编写代码,还要理解它! DevPilot 可以解释不熟悉的代码段,帮助您更快地掌握发生的事情并学习。 +5. **代码解释:** 不仅编写代码,还要理解它! DevPilot 可以解释不熟悉的代码段,帮助您更快地掌握发生的事情并学习。 -6. **自动添加注释:** 保持代码清晰易读! DevPilot 可以自动为您的代码添加注释,确保它易于理解和维护。 - -## 安装插件 - -目前插件对于idea的版本有要求,最低版本要求为2021.2,建议版本为2022.1以上。 - -安装插件有两个渠道: - -Idea官方市场 - 你可以在Intellij IDEA官方[插件市场](https://plugins.jetbrains.com/plugin/23322-devpilot)下载。 - -压缩包安装 - 在plugin页面选择设置中的从磁盘安装,之后将zip包导入即可,压缩包下载地址:下载 -![install from disk](doc/images/screenshot/cn/install_from_disk.png) - -## 配置插件 - -插件安装成功后,在idea设置页面的Tools中就能找到DevPilot的配置页面 -![configuration](doc/images/screenshot/en/config.png) - -在配置页面可以切换插件的中英文,以及选择不同模型和模型的地址,用户名则是在对话时在页面展示时使用。 -![settings](doc/images/screenshot/cn/settings.png) +6. **自动添加注释:** 保持代码清晰易读! DevPilot 可以自动为您的代码添加注释,确保它易于理解和维护。 ## 插件功能 DevPilot为开发者提供了丰富的能力来协助开发者能够省去繁琐的测试用例和注释编写,也能够帮助开发者编写更加高效的代码,同时也能帮助排查代码的问题等等。 -### 生成式AI对话 - -插件支持对话能力,通过右键或者是右边侧边栏的入口可以打开对话的页面 - -![welcome](doc/images/screenshot/cn/welcome.png) - -![menu chat](doc/images/screenshot/cn/menu_chat.png) - -在对话框中可以编写问题,通过回车或者是发送按钮进行聊天问题的发送,后续OpenAI会返回回答并在当前页面输出 - -![chat](doc/images/screenshot/cn/chat.png) - -### 生成测试用例 - -用户可以通过我们插件来生成测试用例。用户选中某个方法右键点击Generate Tests - -![menu testcase](doc/images/screenshot/cn/menu_testcase.png) - -之后窗口会自动打开然后窗口会返回相关的测试用例,用户可以直接复制测试,或者直接选择在光标处插入或者替换相关的代码,甚至还可以新建测试类文件 - -![testcase result](doc/images/screenshot/cn/testcase_result.png) - -### 生成注释 - -插件支持代码注释的生成功能。用户选中代码块后右键点击Generate comments - -![menu gen comments](doc/images/screenshot/cn/menu_gen_comments.png) - -之后窗口自动打开会返回相关的注释结果,并且在编辑器中会自动将生成注释后的结果和原先的代码进行diff,用户可以自行进行比对接受对应的注释。 - -### 修复代码 - -插件支持修复代码的能力。用户选中代码块后右键点击Fix This - -![menu fix bug](doc/images/screenshot/cn/menu_fix_bug.png) - -之后窗口自动打开会返回相关的修复建议,用户可以根据建议修改代码 - -![fix bug result](doc/images/screenshot/cn/fix_bug_result.png) - -### Review 代码 - -插件支持review代码的能力。用户选中代码块后右键点击Review Code - -![menu code review](doc/images/screenshot/cn/menu_code_review.png) - -之后窗口自动打开会返回相关的review结果,用户可以根据结果来编辑自己的代码逻辑 - -![code review result](doc/images/screenshot/cn/code_review_result.png) - -### 性能检测 - -插件支持对代码进行性能检测。用户选中代码块后右键点击Performance Check - -![menu performance check](doc/images/screenshot/cn/menu_performance_check.png) - -之后窗口自动打开会返回相关的性能检测结果,并且在编辑器中会自动将代码优化后的结果和原先的代码进行diff,用户可以根据结果来优化代码 - -![performance check result](doc/images/screenshot/cn/performance_check_result.png) - -### 清理缓存 - -插件整体的相关能力都是附带上下文的,但是上下文可能会存在污染这种情况,并且OpenAI或者是其他的大模型都可能会存在token的限制,超出限制会进行报错。因此我们提供清理的能力,在窗口右上角点击清除按钮就可以将当前会话全部清空。 - -![clear context](doc/images/screenshot/cn/clear_context.png) - -## 路线图 - -在DevPilot,我们一直在不断扩展我们AI驱动的插件的功能,以满足用户不断变化的需求。我们的路线图反映了我们对不断改进和创新的承诺。 - -### 近期目标:与本地LLM集成 - -我们很快将支持将Codellama(这是Meta开发的一个开源LLM)集成到DevPilot中。这将使您能够在本地开发环境中利用LLM的力量,而无需将任何敏感数据发送给OpenAI。 - -通过Codellama集成,用户可以期待: - -- 更准确、与上下文相关的代码建议,Codellama具有10万个上下文长度,因此它提供的建议将基于对代码上下文的更深入理解。这将显著减少开发人员在弄清楚适当代码要使用的时间。 -- 通过在大规模代码库上进行微调,在某些情况下,Codellama可以胜过ChatGPT。 +## 使用文档 -Codellama的使用,请参阅[将Codellama部署到您的本地环境](https://github.com/openpilot-hub/codellama-deploy)。 +[DevPilot JetBrains插件使用文档](https://github.com/openpilot-hub/documentation/blob/main/README_JetBrains.md) -### 长期目标:模型即服务(MaaS)支持 +## RAG -展望未来,我们计划通过支持模型即服务(MaaS)进一步扩展我们的功能。MaaS提供了以服务为基础的方式利用AI模型的功能,而无需本地部署或维护。此功能将使最新的AI模型可无缝访问,而无需承担计算成本或在本地管理这些模型的复杂性。 +[DevPilot RAG使用说明](https://github.com/openpilot-hub/documentation/blob/main/README_RAG.md) -通过将MaaS集成到DevPilot中,我们旨在: +## 构建自己的插件 -- 无需设置OpenAI API密钥即可访问最新的OpenAI模型。 -- 提供即插即用的解决方案来访问最先进的AI模型。 -- 让开发人员在无需本地资源的情况下利用最新的AI进步。 -- 提供可随着项目需求增长而扩展的解决方案。 +[构建 DevPilot](BUILD_PLUGIN_ZH.md) ## 贡献 @@ -143,4 +44,4 @@ Codellama的使用,请参阅[将Codellama部署到您的本地环境](https://gi ## 联系我们 -如果有任何问题或建议,请通过电子邮件联系我们 [pilot_group@zhongan.com](mailto:pilot_group@zhongan.com)。 +如果有任何问题或建议,请通过电子邮件联系我们 [pilot_group@zhongan.com](mailto:pilot_group@zhongan.com)。 diff --git a/build.gradle.kts b/build.gradle.kts index efa9998b..188a4dd9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } group = "com.zhongan" -version = "1.2.0" +version = "2.4.2" repositories { mavenCentral() @@ -17,7 +17,7 @@ intellij { version.set("2022.1.4") type.set("IC") // Target IDE Platform - plugins.set(listOf("com.intellij.java")) + plugins.set(listOf("com.intellij.java","Git4Idea")) } dependencies { @@ -27,6 +27,7 @@ dependencies { implementation("com.squareup.okhttp3:okhttp-sse:4.10.0") implementation("com.vladsch.flexmark:flexmark-all:0.64.8") implementation("org.apache.commons:commons-text:1.10.0") + implementation("com.knuddels:jtokkit:1.0.0") compileOnly("com.puppycrawl.tools:checkstyle:10.9.1") testImplementation("org.mockito:mockito-core:5.7.0") } @@ -36,11 +37,12 @@ tasks { withType { sourceCompatibility = "11" targetCompatibility = "11" + options.encoding = "UTF-8" } patchPluginXml { sinceBuild.set("212") - untilBuild.set("233.*") + untilBuild.set("241.*") pluginDescription.set(provider { file("description.html").readText() }) } @@ -55,6 +57,10 @@ tasks { token.set(System.getenv("PUBLISH_TOKEN")) } + runIde { + systemProperty("devpilot.env", "test") + } + checkstyle { configFile = rootProject.file("checkstyle.xml") maxWarnings = 0 diff --git a/doc/images/screenshot/cn/chat.png b/doc/images/screenshot/cn/chat.png index b590f19d..cf674821 100644 Binary files a/doc/images/screenshot/cn/chat.png and b/doc/images/screenshot/cn/chat.png differ diff --git a/doc/images/screenshot/cn/chat2.png b/doc/images/screenshot/cn/chat2.png deleted file mode 100644 index f8e73116..00000000 Binary files a/doc/images/screenshot/cn/chat2.png and /dev/null differ diff --git a/doc/images/screenshot/cn/clear_context.png b/doc/images/screenshot/cn/clear_context.png index 1a4d903b..5dfd29df 100644 Binary files a/doc/images/screenshot/cn/clear_context.png and b/doc/images/screenshot/cn/clear_context.png differ diff --git a/doc/images/screenshot/cn/code_review_result.png b/doc/images/screenshot/cn/code_review_result.png index ba2714dd..fc4372af 100644 Binary files a/doc/images/screenshot/cn/code_review_result.png and b/doc/images/screenshot/cn/code_review_result.png differ diff --git a/doc/images/screenshot/cn/fix_bug_result.png b/doc/images/screenshot/cn/fix_bug_result.png index 73943821..81903f67 100644 Binary files a/doc/images/screenshot/cn/fix_bug_result.png and b/doc/images/screenshot/cn/fix_bug_result.png differ diff --git a/doc/images/screenshot/cn/performance_check_result.png b/doc/images/screenshot/cn/performance_check_result.png index 0d59b4db..6f7d61b9 100644 Binary files a/doc/images/screenshot/cn/performance_check_result.png and b/doc/images/screenshot/cn/performance_check_result.png differ diff --git a/doc/images/screenshot/cn/welcome.png b/doc/images/screenshot/cn/welcome.png index 77824330..4e566b01 100644 Binary files a/doc/images/screenshot/cn/welcome.png and b/doc/images/screenshot/cn/welcome.png differ diff --git a/doc/images/screenshot/en/chat.png b/doc/images/screenshot/en/chat.png index 32805752..e02d8f72 100644 Binary files a/doc/images/screenshot/en/chat.png and b/doc/images/screenshot/en/chat.png differ diff --git a/doc/images/screenshot/en/chat2.png b/doc/images/screenshot/en/chat2.png deleted file mode 100644 index 192444cc..00000000 Binary files a/doc/images/screenshot/en/chat2.png and /dev/null differ diff --git a/doc/images/screenshot/en/clear_context.png b/doc/images/screenshot/en/clear_context.png index 29b266db..2bb7a9e5 100644 Binary files a/doc/images/screenshot/en/clear_context.png and b/doc/images/screenshot/en/clear_context.png differ diff --git a/doc/images/screenshot/en/code_review_result.png b/doc/images/screenshot/en/code_review_result.png index 7f6c2f22..b0867ed9 100644 Binary files a/doc/images/screenshot/en/code_review_result.png and b/doc/images/screenshot/en/code_review_result.png differ diff --git a/doc/images/screenshot/en/fix_bug_result.png b/doc/images/screenshot/en/fix_bug_result.png index 316b87ff..69255e33 100644 Binary files a/doc/images/screenshot/en/fix_bug_result.png and b/doc/images/screenshot/en/fix_bug_result.png differ diff --git a/doc/images/screenshot/en/performance_check_result.png b/doc/images/screenshot/en/performance_check_result.png index bb7de11d..923eb191 100644 Binary files a/doc/images/screenshot/en/performance_check_result.png and b/doc/images/screenshot/en/performance_check_result.png differ diff --git a/doc/images/screenshot/en/testcase_result.png b/doc/images/screenshot/en/testcase_result.png index 95402f81..b86e2348 100644 Binary files a/doc/images/screenshot/en/testcase_result.png and b/doc/images/screenshot/en/testcase_result.png differ diff --git a/doc/images/screenshot/en/welcome.png b/doc/images/screenshot/en/welcome.png index 3e8ec385..9d4be789 100644 Binary files a/doc/images/screenshot/en/welcome.png and b/doc/images/screenshot/en/welcome.png differ diff --git a/src/main/java/com/zhongan/devpilot/DevPilotIcons.java b/src/main/java/com/zhongan/devpilot/DevPilotIcons.java index b7d91ac9..494f21be 100644 --- a/src/main/java/com/zhongan/devpilot/DevPilotIcons.java +++ b/src/main/java/com/zhongan/devpilot/DevPilotIcons.java @@ -1,9 +1,40 @@ package com.zhongan.devpilot; import com.intellij.openapi.util.IconLoader; +import com.intellij.ui.AnimatedIcon; import javax.swing.Icon; public class DevPilotIcons { public static final Icon SYSTEM_ICON = IconLoader.getIcon("/icons/devpilot.svg", DevPilotIcons.class); + + public static final Icon SYSTEM_ICON_GRAY = IconLoader.getIcon("/icons/devpilot_gray.svg", DevPilotIcons.class); + + public static final Icon SYSTEM_ICON_INLAY = IconLoader.getIcon("icons/devpilot_chat_shortcut.svg", DevPilotIcons.class); + + public static final Icon SYSTEM_ICON_13 = IconLoader.getIcon("/icons/devpilot_13.svg", DevPilotIcons.class); + + public static final Icon SYSTEM_ICON_GRAY_13 = IconLoader.getIcon("/icons/devpilot_gray_13.svg", DevPilotIcons.class); + + public static final Icon COMPLETION_IN_PROGRESS = new AnimatedIcon.Default(); + + public static final Icon LOGIN = IconLoader.getIcon("/icons/login.svg", DevPilotIcons.class); + + public static final Icon LOGIN_DARK = IconLoader.getIcon("/icons/login_dark.svg", DevPilotIcons.class); + + public static final Icon LOGOUT = IconLoader.getIcon("/icons/logout.svg", DevPilotIcons.class); + + public static final Icon LOGOUT_DARK = IconLoader.getIcon("/icons/logout_dark.svg", DevPilotIcons.class); + + public static final Icon SETTINGS = IconLoader.getIcon("/icons/setting.svg", DevPilotIcons.class); + + public static final Icon SETTINGS_DARK = IconLoader.getIcon("/icons/setting_dark.svg", DevPilotIcons.class); + + public static final Icon DISCONNECT = IconLoader.getIcon("/icons/disconnect.svg", DevPilotIcons.class); + + public static final Icon DISCONNECT_DARK = IconLoader.getIcon("/icons/disconnect_dark.svg", DevPilotIcons.class); + + public static final Icon ACCOUNT = IconLoader.getIcon("/icons/account.svg", DevPilotIcons.class); + + public static final Icon ACCOUNT_DARK = IconLoader.getIcon("/icons/account.svg", DevPilotIcons.class); } diff --git a/src/main/java/com/zhongan/devpilot/DevPilotStartupActivity.java b/src/main/java/com/zhongan/devpilot/DevPilotStartupActivity.java index 1a7c3519..b3a722f7 100644 --- a/src/main/java/com/zhongan/devpilot/DevPilotStartupActivity.java +++ b/src/main/java/com/zhongan/devpilot/DevPilotStartupActivity.java @@ -3,6 +3,8 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.StartupActivity; import com.zhongan.devpilot.actions.editor.popupmenu.PopupMenuEditorActionGroupUtil; +import com.zhongan.devpilot.listener.DevPilotFileEditorListener; +import com.zhongan.devpilot.update.DevPilotUpdate; import org.jetbrains.annotations.NotNull; @@ -10,6 +12,9 @@ public class DevPilotStartupActivity implements StartupActivity { @Override public void runActivity(@NotNull Project project) { PopupMenuEditorActionGroupUtil.refreshActions(project); + DevPilotFileEditorListener.registerListener(); + + new DevPilotUpdate.DevPilotUpdateTask(project).queue(); } } diff --git a/src/main/java/com/zhongan/devpilot/DevPilotVersion.java b/src/main/java/com/zhongan/devpilot/DevPilotVersion.java index 7c23c442..e5a49898 100644 --- a/src/main/java/com/zhongan/devpilot/DevPilotVersion.java +++ b/src/main/java/com/zhongan/devpilot/DevPilotVersion.java @@ -19,4 +19,8 @@ public static String getDevPilotVersion() { public static String getIdeaVersion() { return ApplicationInfo.getInstance().getFullVersion(); } + + public static String getVersionName() { + return ApplicationInfo.getInstance().getVersionName(); + } } diff --git a/src/main/java/com/zhongan/devpilot/actions/changesview/GenerateGitCommitMessageAction.java b/src/main/java/com/zhongan/devpilot/actions/changesview/GenerateGitCommitMessageAction.java index 38296d7e..0c7f373d 100644 --- a/src/main/java/com/zhongan/devpilot/actions/changesview/GenerateGitCommitMessageAction.java +++ b/src/main/java/com/zhongan/devpilot/actions/changesview/GenerateGitCommitMessageAction.java @@ -51,7 +51,7 @@ public void actionPerformed(@NotNull AnActionEvent e) { try { String gitDiff = getGitDiff(project, getReferencedFilePaths(e)); - if (DocumentUtil.experienceEstimatedTokens(gitDiff) + DocumentUtil.experienceEstimatedTokens(PromptConst.GENERATE_COMMIT) > DefaultConst.TOKEN_MAX_LENGTH) { + if (DocumentUtil.experienceEstimatedTokens(gitDiff) + DocumentUtil.experienceEstimatedTokens(PromptConst.GENERATE_COMMIT) > DefaultConst.GPT_35_TOKEN_MAX_LENGTH) { DevPilotNotification.warn(DevPilotMessageBundle.get("devpilot.changesview.tokens.estimation.overflow")); } diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/GenerateMethodCommentAction.java b/src/main/java/com/zhongan/devpilot/actions/editor/GenerateMethodCommentAction.java new file mode 100644 index 00000000..60d526b9 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/GenerateMethodCommentAction.java @@ -0,0 +1,27 @@ +package com.zhongan.devpilot.actions.editor; + +import com.zhongan.devpilot.enums.EditorActionEnum; +import com.zhongan.devpilot.util.DevPilotMessageBundle; + +public class GenerateMethodCommentAction extends SelectedCodeGenerateBaseAction { + + @Override + protected String getPrompt() { + return EditorActionEnum.GENERATE_METHOD_COMMENTS.getPrompt(); + } + + @Override + protected EditorActionEnum getEditorActionEnum() { + return EditorActionEnum.GENERATE_METHOD_COMMENTS; + } + + @Override + protected String getShowText() { + return DevPilotMessageBundle.get("devpilot.inlay.shortcut.methodComments"); + } + + @Override + protected void handleValidResult(String result) { + + } +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/SelectedCodeGenerateBaseAction.java b/src/main/java/com/zhongan/devpilot/actions/editor/SelectedCodeGenerateBaseAction.java new file mode 100644 index 00000000..e83a593e --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/SelectedCodeGenerateBaseAction.java @@ -0,0 +1,79 @@ +package com.zhongan.devpilot.actions.editor; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import com.zhongan.devpilot.actions.notifications.DevPilotNotification; +import com.zhongan.devpilot.enums.EditorActionEnum; +import com.zhongan.devpilot.enums.SessionTypeEnum; +import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; +import com.zhongan.devpilot.gui.toolwindows.components.EditorInfo; +import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; +import com.zhongan.devpilot.util.DevPilotMessageBundle; +import com.zhongan.devpilot.util.TokenUtils; +import com.zhongan.devpilot.webview.model.CodeReferenceModel; +import com.zhongan.devpilot.webview.model.MessageModel; + +import java.util.UUID; +import java.util.function.Consumer; + +import org.jetbrains.annotations.NotNull; + +import static com.zhongan.devpilot.actions.editor.popupmenu.PopupMenuEditorActionGroupUtil.validateResult; + +public abstract class SelectedCodeGenerateBaseAction extends AnAction { + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + Project project = e.getProject(); + if (project == null) { + return; + } + + ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow("DevPilot"); + if (toolWindow != null) { + toolWindow.show(); + } + + Consumer callback = result -> { + if (validateResult(result)) { + DevPilotNotification.info(DevPilotMessageBundle.get("devpilot.notification.input.tooLong")); + } + handleValidResult(result); + }; + + Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor(); + String selectedText = editor.getSelectionModel().getSelectedText(); + String prompt = getPrompt().replace("{{selectedCode}}", selectedText); + if (TokenUtils.isInputExceedLimit(prompt)) { + DevPilotNotification.info(DevPilotMessageBundle.get("devpilot.notification.input.tooLong")); + return; + } + + EditorInfo editorInfo = new EditorInfo(editor); + var service = project.getService(DevPilotChatToolWindowService.class); + var username = DevPilotLlmSettingsState.getInstance().getFullName(); + service.clearRequestSession(); + + var showText = getShowText(); + var codeReference = new CodeReferenceModel(editorInfo.getFilePresentableUrl(), + editorInfo.getFileName(), editorInfo.getSelectedStartLine(), editorInfo.getSelectedEndLine(), getEditorActionEnum()); + + var codeMessage = MessageModel.buildCodeMessage( + UUID.randomUUID().toString(), System.currentTimeMillis(), showText, username, codeReference); + + service.sendMessage(SessionTypeEnum.MULTI_TURN.getCode(), prompt, callback, codeMessage); + } + + protected abstract String getPrompt(); + + protected abstract EditorActionEnum getEditorActionEnum(); + + protected abstract String getShowText(); + + protected abstract void handleValidResult(String result); +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/inlay/ChatShortcutHintBaseProvider.java b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/ChatShortcutHintBaseProvider.java new file mode 100644 index 00000000..35b96c73 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/ChatShortcutHintBaseProvider.java @@ -0,0 +1,76 @@ +package com.zhongan.devpilot.actions.editor.inlay; + +import com.intellij.codeInsight.hints.ImmediateConfigurable; +import com.intellij.codeInsight.hints.InlayHintsCollector; +import com.intellij.codeInsight.hints.InlayHintsProvider; +import com.intellij.codeInsight.hints.InlayHintsSink; +import com.intellij.codeInsight.hints.NoSettings; +import com.intellij.codeInsight.hints.SettingsKey; +import com.intellij.lang.Language; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiFile; + +import java.util.List; + +import javax.swing.JPanel; + +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ChatShortcutHintBaseProvider implements InlayHintsProvider { + + private List supportedElementTypes; + + public ChatShortcutHintBaseProvider(List supportedElementTypes) { + this.supportedElementTypes = supportedElementTypes; + } + + @Override + public boolean isVisibleInSettings() { + return true; + } + + @NotNull + @Override + public SettingsKey getKey() { + return new SettingsKey<>("DevPilot.chat.shortcut.provider"); + } + + @Nls(capitalization = Nls.Capitalization.Sentence) + @NotNull + @Override + public String getName() { + return "DevPilot.chat.shortcut"; + } + + @Nullable + @Override + public String getPreviewText() { + return null; + } + + @NotNull + @Override + public ImmediateConfigurable createConfigurable(@NotNull NoSettings noSettings) { + return changeListener -> new JPanel(); + } + + @NotNull + @Override + public NoSettings createSettings() { + return new NoSettings(); + } + + @Nullable + @Override + public InlayHintsCollector getCollectorFor(@NotNull PsiFile psiFile, @NotNull Editor editor, + @NotNull NoSettings noSettings, @NotNull InlayHintsSink inlayHintsSink) { + return new ChatShortcutHintCollector(editor, supportedElementTypes); + } + + @Override + public boolean isLanguageSupported(@NotNull Language language) { + return true; + } +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/inlay/ChatShortcutHintCollector.java b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/ChatShortcutHintCollector.java new file mode 100644 index 00000000..e7abed9d --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/ChatShortcutHintCollector.java @@ -0,0 +1,148 @@ +package com.zhongan.devpilot.actions.editor.inlay; + +import com.intellij.codeInsight.hints.FactoryInlayHintsCollector; +import com.intellij.codeInsight.hints.InlayHintsSink; +import com.intellij.codeInsight.hints.presentation.InlayPresentation; +import com.intellij.codeInsight.hints.presentation.PresentationFactory; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.actionSystem.impl.SimpleDataContext; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.CaretModel; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiWhiteSpace; +import com.intellij.psi.util.PsiUtilCore; +import com.zhongan.devpilot.DevPilotIcons; +import com.zhongan.devpilot.enums.EditorActionEnum; +import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; +import com.zhongan.devpilot.settings.state.ChatShortcutSettingState; +import com.zhongan.devpilot.util.DevPilotMessageBundle; + +import java.util.List; + +import org.jetbrains.annotations.NotNull; + +public class ChatShortcutHintCollector extends FactoryInlayHintsCollector { + + private List supportedElementTypes; + + protected PresentationFactory factory; + + private DevPilotChatToolWindowService service; + + private Editor editor; + + public ChatShortcutHintCollector(@NotNull Editor editor, List supportedElementsType) { + super(editor); + this.editor = editor; + this.supportedElementTypes = supportedElementsType; + this.factory = this.getFactory(); + this.service = editor.getProject().getService(DevPilotChatToolWindowService.class); + } + + @Override + public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink) { + if (ChatShortcutSettingState.getInstance().getEnable() && + supportedElementTypes.contains(PsiUtilCore.getElementType(psiElement).toString())) { + + inlayHintsSink.addBlockElement(getAnchorOffset(psiElement), true, true, 1000, + factory.seq(factory.textSpacePlaceholder(computeInitialWhitespace(editor, psiElement), false), + factory.icon(DevPilotIcons.SYSTEM_ICON_INLAY), + buildClickableTextChatShortcutEntry(" " + DevPilotMessageBundle.get("devpilot.inlay.shortcut.explain") + + " | ", EditorActionEnum.EXPLAIN_THIS, psiElement), + buildClickableTextChatShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.fix") + + " | ", EditorActionEnum.FIX_THIS, psiElement), + buildClickableTextChatShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.inlineComment") + + " | ", EditorActionEnum.GENERATE_COMMENTS, psiElement), + buildClickableMethodCommentsShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.methodComments") + + " | ", psiElement), + buildClickableTextChatShortcutEntry(DevPilotMessageBundle.get("devpilot.inlay.shortcut.test"), + EditorActionEnum.GENERATE_TESTS, psiElement))); + } + return true; + } + + private InlayPresentation buildClickableTextChatShortcutEntry(String text, EditorActionEnum actionEnum, PsiElement psiElement) { + return factory.seq(factory.referenceOnHover(factory.smallText(text), (mouseEvent, point) -> { + TextRange textRange = psiElement.getTextRange(); + editor.getSelectionModel().setSelection(textRange.getStartOffset(), textRange.getEndOffset()); + + service.handleActions(actionEnum); + })); + } + + private InlayPresentation buildClickableMethodCommentsShortcutEntry(String text, PsiElement psiElement) { + return factory.seq(factory.referenceOnHover(factory.smallText(text), (mouseEvent, point) -> { + TextRange textRange = psiElement.getTextRange(); + editor.getSelectionModel().setSelection(textRange.getStartOffset(), textRange.getEndOffset()); + + ApplicationManager.getApplication().invokeLater(() -> { + moveCareToPreviousLineStart(editor, textRange.getStartOffset()); + AnAction action = ActionManager.getInstance().getAction("com.zhongan.devpilot.actions.editor.generate.method.comments"); + DataContext context = SimpleDataContext.getProjectContext(editor.getProject()); + action.actionPerformed(new AnActionEvent(null, context, "", new Presentation(), ActionManager.getInstance(), 0)); + }); + })); + } + + private static int getAnchorOffset(@NotNull PsiElement psiElement) { + int anchorOffset = psiElement.getTextRange().getStartOffset(); + PsiElement[] children = psiElement.getChildren(); + for (PsiElement element : children) { + if (!(element instanceof PsiComment) && !(element instanceof PsiWhiteSpace)) { + anchorOffset = element.getTextRange().getStartOffset(); + break; + } + } + return anchorOffset; + } + + private int computeInitialWhitespace(Editor editor, PsiElement psiElement) { + int lineNum = editor.getDocument().getLineNumber(psiElement.getTextRange().getStartOffset()); + String textOnLine = editor.getDocument().getText(new TextRange(editor.getDocument().getLineStartOffset(lineNum), + editor.getDocument().getLineEndOffset(lineNum))); + + int whitespaceCounter = 0; + for (char character : textOnLine.toCharArray()) { + if (Character.isWhitespace(character)) { + whitespaceCounter++; + } else { + break; + } + } + + return whitespaceCounter; + } + + public void moveCareToPreviousLineStart(Editor editor, int offset) { + Project project = editor.getProject(); + int previousLineNumber = getPreviousLineNumber(project, offset); + if (previousLineNumber == -1) { + return; + } + int lineStartOffset = editor.getDocument().getLineStartOffset(previousLineNumber); + CaretModel caretModel = editor.getCaretModel(); + caretModel.moveToOffset(lineStartOffset); + } + + public int getPreviousLineNumber(Project project, int offset) { + Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor(); + + if (editor != null) { + Document document = editor.getDocument(); + int lineNumber = document.getLineNumber(offset); + return lineNumber > 0 ? lineNumber - 1 : -1; + } + + return -1; + } +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/GoChatShortcutHintProvider.java b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/GoChatShortcutHintProvider.java new file mode 100644 index 00000000..0f68e6e3 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/GoChatShortcutHintProvider.java @@ -0,0 +1,12 @@ +package com.zhongan.devpilot.actions.editor.inlay.languages; + +import com.zhongan.devpilot.actions.editor.inlay.ChatShortcutHintBaseProvider; + +import java.util.List; + +public class GoChatShortcutHintProvider extends ChatShortcutHintBaseProvider { + + public GoChatShortcutHintProvider() { + super(List.of("FUNCTION_DECLARATION", "METHOD_DECLARATION")); + } +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/JavaChatShortcutHintProvider.java b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/JavaChatShortcutHintProvider.java new file mode 100644 index 00000000..a5497888 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/JavaChatShortcutHintProvider.java @@ -0,0 +1,12 @@ +package com.zhongan.devpilot.actions.editor.inlay.languages; + +import com.zhongan.devpilot.actions.editor.inlay.ChatShortcutHintBaseProvider; + +import java.util.List; + +public class JavaChatShortcutHintProvider extends ChatShortcutHintBaseProvider { + + public JavaChatShortcutHintProvider() { + super(List.of("METHOD")); + } +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/JavaScriptChatShortcutHintProvider.java b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/JavaScriptChatShortcutHintProvider.java new file mode 100644 index 00000000..65558164 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/JavaScriptChatShortcutHintProvider.java @@ -0,0 +1,12 @@ +package com.zhongan.devpilot.actions.editor.inlay.languages; + +import com.zhongan.devpilot.actions.editor.inlay.ChatShortcutHintBaseProvider; + +import java.util.List; + +public class JavaScriptChatShortcutHintProvider extends ChatShortcutHintBaseProvider { + + public JavaScriptChatShortcutHintProvider() { + super(List.of("JS:FUNCTION_DECLARATION", "JS:FUNCTION_EXPRESSION")); + } +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/PythonChatShortcutHintProvider.java b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/PythonChatShortcutHintProvider.java new file mode 100644 index 00000000..7cc2b9b0 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/PythonChatShortcutHintProvider.java @@ -0,0 +1,12 @@ +package com.zhongan.devpilot.actions.editor.inlay.languages; + +import com.zhongan.devpilot.actions.editor.inlay.ChatShortcutHintBaseProvider; + +import java.util.List; + +public class PythonChatShortcutHintProvider extends ChatShortcutHintBaseProvider { + + public PythonChatShortcutHintProvider() { + super(List.of("Py:FUNCTION_DECLARATION")); + } +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/TypeScriptChatShortcutHintProvider.java b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/TypeScriptChatShortcutHintProvider.java new file mode 100644 index 00000000..234ec4aa --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/actions/editor/inlay/languages/TypeScriptChatShortcutHintProvider.java @@ -0,0 +1,12 @@ +package com.zhongan.devpilot.actions.editor.inlay.languages; + +import com.zhongan.devpilot.actions.editor.inlay.ChatShortcutHintBaseProvider; + +import java.util.List; + +public class TypeScriptChatShortcutHintProvider extends ChatShortcutHintBaseProvider { + + public TypeScriptChatShortcutHintProvider() { + super(List.of("JS:TYPESCRIPT_FUNCTION", "JS:TYPESCRIPT_FUNCTION_EXPRESSION")); + } +} diff --git a/src/main/java/com/zhongan/devpilot/actions/editor/popupmenu/PopupMenuEditorActionGroupUtil.java b/src/main/java/com/zhongan/devpilot/actions/editor/popupmenu/PopupMenuEditorActionGroupUtil.java index 58e748f4..f5428977 100644 --- a/src/main/java/com/zhongan/devpilot/actions/editor/popupmenu/PopupMenuEditorActionGroupUtil.java +++ b/src/main/java/com/zhongan/devpilot/actions/editor/popupmenu/PopupMenuEditorActionGroupUtil.java @@ -23,9 +23,9 @@ import com.zhongan.devpilot.util.DevPilotMessageBundle; import com.zhongan.devpilot.util.DocumentUtil; import com.zhongan.devpilot.util.LanguageUtil; -import com.zhongan.devpilot.util.PerformanceCheckUtils; import com.zhongan.devpilot.util.PromptTemplate; import com.zhongan.devpilot.util.PsiFileUtil; +import com.zhongan.devpilot.util.TokenUtils; import com.zhongan.devpilot.webview.model.CodeReferenceModel; import com.zhongan.devpilot.webview.model.MessageModel; @@ -47,12 +47,12 @@ public class PopupMenuEditorActionGroupUtil { private static final Map ICONS = new LinkedHashMap<>(Map.of( - EditorActionEnum.PERFORMANCE_CHECK.getLabel(), AllIcons.Plugins.Updated, - EditorActionEnum.GENERATE_COMMENTS.getLabel(), AllIcons.Actions.InlayRenameInCommentsActive, - EditorActionEnum.GENERATE_TESTS.getLabel(), AllIcons.Modules.GeneratedTestRoot, - EditorActionEnum.FIX_THIS.getLabel(), AllIcons.Actions.QuickfixBulb, - EditorActionEnum.REVIEW_CODE.getLabel(), AllIcons.Actions.PreviewDetailsVertically, - EditorActionEnum.EXPLAIN_THIS.getLabel(), AllIcons.Actions.Preview)); + EditorActionEnum.PERFORMANCE_CHECK.getLabel(), AllIcons.Plugins.Updated, + EditorActionEnum.GENERATE_COMMENTS.getLabel(), AllIcons.Actions.InlayRenameInCommentsActive, + EditorActionEnum.GENERATE_TESTS.getLabel(), AllIcons.Modules.GeneratedTestRoot, + EditorActionEnum.FIX_THIS.getLabel(), AllIcons.Actions.QuickfixBulb, + EditorActionEnum.REVIEW_CODE.getLabel(), AllIcons.Actions.PreviewDetailsVertically, + EditorActionEnum.EXPLAIN_THIS.getLabel(), AllIcons.Actions.Preview)); public static void refreshActions(Project project) { AnAction actionGroup = ActionManager.getInstance().getAction("com.zhongan.devpilot.actions.editor.popupmenu.BasicEditorAction"); @@ -69,7 +69,7 @@ public static void refreshActions(Project project) { protected void actionPerformed(Project project, Editor editor, String selectedText) { ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow("DevPilot"); toolWindow.show(); - if (isInputExceedLimit(selectedText, prompt)) { + if (TokenUtils.isInputExceedLimit(selectedText, prompt)) { DevPilotNotification.info(DevPilotMessageBundle.get("devpilot.notification.input.tooLong")); return; } @@ -80,21 +80,14 @@ protected void actionPerformed(Project project, Editor editor, String selectedTe } Consumer callback = result -> { + DevPilotNotification.debug("result is -> [." + result + "]."); if (validateResult(result)) { DevPilotNotification.info(DevPilotMessageBundle.get("devpilot.notification.input.tooLong")); return; } - switch (editorActionEnum) { - case PERFORMANCE_CHECK: - // display result, and open diff window - PerformanceCheckUtils.showDiffWindow(selectedText, project, editor); - break; - case GENERATE_COMMENTS: - DocumentUtil.diffCommentAndFormatWindow(project, editor, result); - break; - default: - break; + if (editorActionEnum == EditorActionEnum.GENERATE_COMMENTS) { + DocumentUtil.diffCommentAndFormatWindow(project, editor, result); } }; @@ -113,7 +106,8 @@ protected void actionPerformed(Project project, Editor editor, String selectedTe } }); } - if (LanguageSettingsState.getInstance().getLanguageIndex() == 1) { + if (LanguageSettingsState.getInstance().getLanguageIndex() == 1 + && editorActionEnum != EditorActionEnum.GENERATE_COMMENTS) { promptTemplate.appendLast(PromptConst.ANSWER_IN_CHINESE); } @@ -122,11 +116,11 @@ protected void actionPerformed(Project project, Editor editor, String selectedTe service.clearRequestSession(); var showText = DevPilotMessageBundle.get(label); - var codeReference = new CodeReferenceModel(editorInfo.getFileUrl(), - editorInfo.getFileName(), editorInfo.getSelectedStartLine(), editorInfo.getSelectedEndLine(), editorActionEnum); + var codeReference = new CodeReferenceModel(editorInfo.getFilePresentableUrl(), + editorInfo.getFileName(), editorInfo.getSelectedStartLine(), editorInfo.getSelectedEndLine(), editorActionEnum); var codeMessage = MessageModel.buildCodeMessage( - UUID.randomUUID().toString(), System.currentTimeMillis(), showText, username, codeReference); + UUID.randomUUID().toString(), System.currentTimeMillis(), showText, username, codeReference); service.sendMessage(SessionTypeEnum.MULTI_TURN.getCode(), promptTemplate.getPrompt(), callback, codeMessage); } @@ -151,24 +145,8 @@ public static void registerOrReplaceAction(AnAction action) { * * @return */ - private static boolean validateResult(String content) { - return content.contains(DefaultConst.MAX_TOKEN_EXCEPTION_MSG); - } - - /** - * check length of input rather than max limit - * 1 token = 3 english character() - * - * @param content - * @return - */ - private static boolean isInputExceedLimit(String content, String prompt) { - // text too long, openai server always timeout - if (content.length() + prompt.length() > DefaultConst.TOKEN_MAX_LENGTH) { - return true; - } - // valid chinese and english character length - return DocumentUtil.experienceEstimatedTokens(content + prompt) > DefaultConst.TOKEN_MAX_LENGTH; + public static boolean validateResult(String content) { + return content.contains(DefaultConst.GPT_35_MAX_TOKEN_EXCEPTION_MSG); } } diff --git a/src/main/java/com/zhongan/devpilot/actions/notifications/DevPilotNotification.java b/src/main/java/com/zhongan/devpilot/actions/notifications/DevPilotNotification.java index f9e10235..0a3a93d7 100644 --- a/src/main/java/com/zhongan/devpilot/actions/notifications/DevPilotNotification.java +++ b/src/main/java/com/zhongan/devpilot/actions/notifications/DevPilotNotification.java @@ -1,10 +1,20 @@ package com.zhongan.devpilot.actions.notifications; +import com.intellij.ide.BrowserUtil; import com.intellij.notification.Notification; +import com.intellij.notification.NotificationAction; import com.intellij.notification.NotificationType; import com.intellij.notification.Notifications; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.zhongan.devpilot.settings.state.AIGatewaySettingsState; +import com.zhongan.devpilot.update.DevPilotUpdate; import com.zhongan.devpilot.util.DevPilotMessageBundle; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + public class DevPilotNotification { public static void info(String content) { @@ -34,4 +44,65 @@ public static void error(String content) { Notifications.Bus.notify(notification); } + public static void linkInfo(String content, String text, String url) { + var notification = new Notification( + "DevPilot Notification Group", + DevPilotMessageBundle.get("notification.group.devpilot"), + content, + NotificationType.INFORMATION); + + notification.addAction(new NotificationAction(text) { + @Override + public void actionPerformed(@NotNull AnActionEvent e, @NotNull Notification notification) { + BrowserUtil.browse(url); + } + }); + + Notifications.Bus.notify(notification); + } + + public static void infoAndAction(String content, String display, String url) { + var notification = new Notification( + "DevPilot Notification Group", + DevPilotMessageBundle.get("notification.group.devpilot"), + content, + NotificationType.INFORMATION); + notification.addAction(new NotificationAction(display) { + @Override + public void actionPerformed(@NotNull AnActionEvent anActionEvent, @NotNull Notification notification) { + BrowserUtil.browse(url); + } + }); + Notifications.Bus.notify(notification); + } + + public static void debug(String content) { + var selectedModel = AIGatewaySettingsState.getInstance().getSelectedModel(); + var host = AIGatewaySettingsState.getInstance().getModelBaseHost(selectedModel); + if (StringUtils.endsWith(host, "dev") || StringUtils.endsWith(host, "prd")) { + var notification = new Notification( + "DevPilot Notification Group", + DevPilotMessageBundle.get("notification.group.devpilot"), + content, + NotificationType.ERROR); + Notifications.Bus.notify(notification); + } + } + + public static void updateNotification(Project project) { + var notification = new Notification( + "DevPilot Notification Group", + DevPilotMessageBundle.get("notification.group.devpilot"), + DevPilotMessageBundle.get("devpilot.notification.update.message"), + NotificationType.IDE_UPDATE); + notification.addAction(NotificationAction + .createSimpleExpiring(DevPilotMessageBundle.get("devpilot.notification.installButton"), () -> { + ApplicationManager.getApplication() + .executeOnPooledThread(() -> DevPilotUpdate.installUpdate(project)); + })); + notification.addAction(NotificationAction + .createSimpleExpiring(DevPilotMessageBundle.get("devpilot.notification.hideButton"), () -> { + })); + Notifications.Bus.notify(notification); + } } diff --git a/src/main/java/com/zhongan/devpilot/completions/Completion.java b/src/main/java/com/zhongan/devpilot/completions/Completion.java new file mode 100644 index 00000000..ee04321d --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/Completion.java @@ -0,0 +1,5 @@ +package com.zhongan.devpilot.completions; + +public interface Completion { + boolean isSnippet(); +} diff --git a/src/main/java/com/zhongan/devpilot/completions/CompletionUtils.java b/src/main/java/com/zhongan/devpilot/completions/CompletionUtils.java new file mode 100644 index 00000000..36bffa82 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/CompletionUtils.java @@ -0,0 +1,91 @@ +package com.zhongan.devpilot.completions; + +import com.intellij.codeInsight.completion.CompletionParameters; +import com.intellij.codeInsight.completion.CompletionResultSet; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import com.zhongan.devpilot.completions.general.SuggestionTrigger; +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; +import com.zhongan.devpilot.completions.requests.ResultEntry; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.zhongan.devpilot.completions.general.StaticConfig.MAX_COMPLETIONS; +import static com.zhongan.devpilot.completions.general.Utils.endsWithADot; + +public class CompletionUtils { + + @Nullable + private static String getCursorPrefix(@NotNull Document document, int cursorPosition) { + try { + int lineNumber = document.getLineNumber(cursorPosition); + int lineStart = document.getLineStartOffset(lineNumber); + + return document.getText(TextRange.create(lineStart, cursorPosition)).trim(); + } catch (Throwable e) { + Logger.getInstance(CompletionUtils.class).warn("Failed to get cursor prefix: ", e); + return null; + } + } + + @Nullable + private static String getCursorSuffix(@NotNull Document document, int cursorPosition) { + try { + int lineNumber = document.getLineNumber(cursorPosition); + int lineEnd = document.getLineEndOffset(lineNumber); + + return document.getText(TextRange.create(cursorPosition, lineEnd)).trim(); + } catch (Throwable e) { + Logger.getInstance(CompletionUtils.class).warn("Failed to get cursor suffix: ", e); + return null; + } + } + + @Nullable + public static DevPilotCompletion createDevpilotCompletion( + @NotNull Document document, + int offset, + String oldPrefix, + ResultEntry result, + int index, + SuggestionTrigger suggestionTrigger) { + String cursorPrefix = CompletionUtils.getCursorPrefix(document, offset); + String cursorSuffix = CompletionUtils.getCursorSuffix(document, offset); + if (cursorPrefix == null || cursorSuffix == null) { + return null; + } + + return new DevPilotCompletion( + result.id, + oldPrefix, + result.newPrefix, + result.oldSuffix, + result.newSuffix, + index, + cursorPrefix, + cursorSuffix, + result.completionMetadata, + suggestionTrigger); + } + + public static int completionLimit( + CompletionParameters parameters, CompletionResultSet result, boolean isLocked) { + return completionLimit( + parameters.getEditor().getDocument(), + result.getPrefixMatcher().getPrefix(), + parameters.getOffset(), + isLocked); + } + + public static int completionLimit( + @NotNull Document document, @NotNull String prefix, int offset, boolean isLocked) { + if (isLocked) { + return 1; + } + boolean preferDevPilot = !endsWithADot(document, offset - prefix.length()); + + return preferDevPilot ? MAX_COMPLETIONS : 1; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/autoimport/handler/AutoImportHandler.java b/src/main/java/com/zhongan/devpilot/completions/autoimport/handler/AutoImportHandler.java new file mode 100644 index 00000000..32213841 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/autoimport/handler/AutoImportHandler.java @@ -0,0 +1,43 @@ +package com.zhongan.devpilot.completions.autoimport.handler; + +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.zhongan.devpilot.completions.autoimport.importer.DevpilotReferenceImporter; +import com.zhongan.devpilot.completions.autoimport.importer.java.DevpilotJavaReferenceImporter; + + +public class AutoImportHandler { + + private int startOffset; + + private int endOffset; + + private Editor myEditor; + + private PsiFile myFile; + + + public AutoImportHandler(int startOffset, int endOffset, Editor editor, PsiFile file) { + this.startOffset = startOffset; + this.endOffset = endOffset; + this.myEditor = editor; + this.myFile = file; + } + + public void invoke() { + if (myEditor.isDisposed() || myFile.getProject().isDisposed()) return; + VirtualFile virtualFile = myFile.getVirtualFile(); + DevpilotReferenceImporter importer = null; + if (isJavaFile(virtualFile)) { + importer = new DevpilotJavaReferenceImporter(myEditor, myFile, startOffset, endOffset); + } + if (importer != null) { + importer.computeReferences(); + } + } + + public static boolean isJavaFile(VirtualFile file) { + return file != null && file.getName().toLowerCase().endsWith(".java"); + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/autoimport/importer/DevpilotReferenceImporter.java b/src/main/java/com/zhongan/devpilot/completions/autoimport/importer/DevpilotReferenceImporter.java new file mode 100644 index 00000000..86d3b4a3 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/autoimport/importer/DevpilotReferenceImporter.java @@ -0,0 +1,28 @@ +package com.zhongan.devpilot.completions.autoimport.importer; + +import com.intellij.codeInsight.daemon.ReferenceImporter; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiFile; + +import java.lang.reflect.Method; + +public class DevpilotReferenceImporter { + + protected Editor myEditor; + protected PsiFile myFile; + protected int startOffset; + protected int endOffset; + + protected DevpilotReferenceImporter(Editor editor, PsiFile file, int startOffset, int endOffset) { + myEditor = editor; + myFile = file; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + public void computeReferences(){} + + protected void callComputeReferences(ReferenceImporter referenceImporter, Method method) {} + + protected void manualImportReference() {} +} diff --git a/src/main/java/com/zhongan/devpilot/completions/autoimport/importer/java/DevpilotJavaReferenceImporter.java b/src/main/java/com/zhongan/devpilot/completions/autoimport/importer/java/DevpilotJavaReferenceImporter.java new file mode 100644 index 00000000..8e7cd177 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/autoimport/importer/java/DevpilotJavaReferenceImporter.java @@ -0,0 +1,102 @@ +package com.zhongan.devpilot.completions.autoimport.importer.java; + +import com.intellij.codeInsight.daemon.impl.CollectHighlightsUtil; +import com.intellij.codeInsight.daemon.impl.quickfix.ImportClassFix; +import com.intellij.codeInsight.daemon.impl.quickfix.ImportClassFixBase; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiJavaCodeReferenceElement; +import com.zhongan.devpilot.completions.autoimport.importer.DevpilotReferenceImporter; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + + +public class DevpilotJavaReferenceImporter extends DevpilotReferenceImporter { + public DevpilotJavaReferenceImporter(Editor editor, PsiFile file, int startOffset, int endOffset) { + super(editor, file, startOffset, endOffset); + } + + @Override + public void computeReferences() { + this.manualImportReference(); + } + + protected void manualImportReference() { + Document document = myEditor.getDocument(); + int importLineNumber = document.getLineNumber(startOffset); + while (importLineNumber < document.getLineCount()) { + List fixes = computeImportFix(myFile, document.getLineStartOffset(importLineNumber), document.getLineEndOffset(importLineNumber)); + if (fixes != null && !fixes.isEmpty()) { + boolean alreadyImported = false; + for (ImportClassFix fix : fixes) { + ImportClassFixBase.Result result = loopManualImportReference(fix); + if (result == ImportClassFixBase.Result.CLASS_AUTO_IMPORTED) { + alreadyImported = true; + break; + } + } + if (!alreadyImported) { + importLineNumber++; + } + } + else { + importLineNumber++; + } + } + } + + protected ImportClassFixBase.Result loopManualImportReference(ImportClassFix fix) { + FutureTask writetask = new FutureTask(() -> { + try { + return fix.doFix(myEditor, false, true, true); + } catch (Exception e) { + return ImportClassFixBase.Result.POPUP_NOT_SHOWN; + } + }); + ApplicationManager.getApplication().invokeLater(() -> { + ApplicationManager.getApplication().runWriteAction(writetask); + }); + try { + return writetask.get(); + } catch (InterruptedException | ExecutionException e) { + return ImportClassFixBase.Result.POPUP_NOT_SHOWN; + } + } + + private List computeImportFix(PsiFile file, int startOffset, int endOffset) { + FutureTask> readElementTask = new FutureTask<>(() ->CollectHighlightsUtil.getElementsInRange(file, startOffset, endOffset) ); + ApplicationManager.getApplication().runReadAction(readElementTask); + List elements; + try { + elements = readElementTask.get(); + } catch (Exception e){ + return null; + } + List finalElements = elements; + FutureTask> readFixTask = new FutureTask<>(() -> { + List importFixes = new ArrayList<>(); + for (PsiElement element : finalElements) { + if (element instanceof PsiJavaCodeReferenceElement) { + PsiJavaCodeReferenceElement ref = (PsiJavaCodeReferenceElement)element; + ImportClassFix fix = new ImportClassFix(ref); + if (fix.isAvailable(file.getProject(), null, file)) { + importFixes.add(fix); + } + } + } + return importFixes; + }); + ApplicationManager.getApplication().runReadAction(readFixTask); + try { + return readFixTask.get(); + } catch (Exception e){ + return null; + } + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/general/CompletionKind.java b/src/main/java/com/zhongan/devpilot/completions/general/CompletionKind.java new file mode 100644 index 00000000..6843b4ba --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/general/CompletionKind.java @@ -0,0 +1,7 @@ +package com.zhongan.devpilot.completions.general; + +public enum CompletionKind { + Classic, + Line, + Snippet +} diff --git a/src/main/java/com/zhongan/devpilot/completions/general/DependencyContainer.java b/src/main/java/com/zhongan/devpilot/completions/general/DependencyContainer.java new file mode 100644 index 00000000..3cee3dda --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/general/DependencyContainer.java @@ -0,0 +1,50 @@ +package com.zhongan.devpilot.completions.general; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializer; +import com.zhongan.devpilot.completions.inline.InlineCompletionHandler; +import com.zhongan.devpilot.completions.prediction.CompletionFacade; + +import org.jetbrains.annotations.NotNull; + +public class DependencyContainer { + private static InlineCompletionHandler inlineCompletionHandler = null; + + private static Gson gson = instanceOfGson(); + + public static synchronized Gson instanceOfGson() { + if (gson == null) { + gson = new GsonBuilder().registerTypeAdapter(Double.class, doubleOrIntSerializer()).create(); + } + + return gson; + } + + public static InlineCompletionHandler singletonOfInlineCompletionHandler() { + if (inlineCompletionHandler == null) { + inlineCompletionHandler = + new InlineCompletionHandler( + instanceOfCompletionFacade() + ); + } + + return inlineCompletionHandler; + } + + @NotNull + public static CompletionFacade instanceOfCompletionFacade() { + return new CompletionFacade(); + } + + @NotNull + private static JsonSerializer doubleOrIntSerializer() { + return (src, type, jsonSerializationContext) -> { + if (src == src.longValue()) { + return new JsonPrimitive(src.longValue()); + } + return new JsonPrimitive(src); + }; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/general/EditorUtils.java b/src/main/java/com/zhongan/devpilot/completions/general/EditorUtils.java new file mode 100644 index 00000000..6c60954f --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/general/EditorUtils.java @@ -0,0 +1,11 @@ +package com.zhongan.devpilot.completions.general; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorKind; + +public class EditorUtils { + public static boolean isMainEditor(Editor editor) { + return editor.getEditorKind() == EditorKind.MAIN_EDITOR || ApplicationManager.getApplication().isUnitTestMode(); + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/general/StaticConfig.java b/src/main/java/com/zhongan/devpilot/completions/general/StaticConfig.java new file mode 100644 index 00000000..e503bd5c --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/general/StaticConfig.java @@ -0,0 +1,46 @@ +package com.zhongan.devpilot.completions.general; + +public class StaticConfig { + public static final int MAX_COMPLETIONS = 5; + + // 100 KB + public static final int DEFALUT_MAX_OFFSET = 100000; + + // 100 KB + public static final int MAX_OFFSET = 100000; + + //15kb + public static final int PREFIX_MAX_OFFSET = 15000; + + //2500 + public static final int SUFFIX_MAX_OFFSET = 2500; + + // 200 B + public static final int MIN_OFFSET = 200; + + // 2000 B + public static final int MAX_CHAT_COMPLETION_MESSAGE_LENGTH = 2000; + + //1000 + public static final int MAX_INSTRUCT_COMPLETION_TOKENS = 1000; + + // 100 B + public static final String MIN_CHAT_COMPLETION_MESSAGE_LENGTH = "200"; + + public static final int MIN_USER_PREFIX_LENGTH = 5; + + public static final int MIN_DELAY_TIME_IN_MILLIS = 6000; + + public static final int MAX_DELAY_TIME_IN_MILLIS = 12000; + + public static final Long DEBOUNCE_VALUE_300 = 300L; + + public static final Long DEBOUNCE_VALUE_600 = 600L; + + public static final Long DEBOUNCE_VALUE_900 = 900L; + + public static final Long DEBOUNCE_VALUE_1200 = 1200L; + + public static final Long DEBOUNCE_VALUE_3000 = 3000L; + +} diff --git a/src/main/java/com/zhongan/devpilot/completions/general/SuggestionTrigger.java b/src/main/java/com/zhongan/devpilot/completions/general/SuggestionTrigger.java new file mode 100644 index 00000000..58a5d262 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/general/SuggestionTrigger.java @@ -0,0 +1,6 @@ +package com.zhongan.devpilot.completions.general; + +public enum SuggestionTrigger { + DocumentChanged, + LookAhead +} diff --git a/src/main/java/com/zhongan/devpilot/completions/general/Utils.java b/src/main/java/com/zhongan/devpilot/completions/general/Utils.java new file mode 100644 index 00000000..aaea38ac --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/general/Utils.java @@ -0,0 +1,64 @@ +package com.zhongan.devpilot.completions.general; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.TextRange; +import com.intellij.util.concurrency.AppExecutorUtil; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class Utils { + + public static boolean endsWithADot(Document doc, int positionBeforeSuggestionPrefix) { + int begin = positionBeforeSuggestionPrefix - ".".length(); + if (begin < 0 || positionBeforeSuggestionPrefix > doc.getTextLength()) { + return false; + } else { + String tail = doc.getText(new TextRange(begin, positionBeforeSuggestionPrefix)); + return tail.equals("."); + } + } + + @NotNull + public static Integer toInt(@Nullable Long aLong) { + if (aLong == null) { + return 0; + } + + return Math.toIntExact(aLong); + } + + public static List asLines(String block) { + return Arrays.stream(block.split("\n")).collect(Collectors.toList()); + } + + public static Future executeThread(Runnable runnable) { + if (isUnitTestMode()) { + runnable.run(); + return CompletableFuture.completedFuture(null); + } + return AppExecutorUtil.getAppExecutorService().submit(runnable); + } + + public static Future executeThread(Runnable runnable, long delay, TimeUnit timeUnit) { + if (isUnitTestMode()) { + runnable.run(); + return CompletableFuture.completedFuture(null); + } + return AppExecutorUtil.getAppScheduledExecutorService().schedule(runnable, delay, timeUnit); + } + + public static boolean isUnitTestMode() { + return ApplicationManager.getApplication() == null + || ApplicationManager.getApplication().isUnitTestMode(); + } + +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/AcceptDevPilotInlineCompletionAction.java b/src/main/java/com/zhongan/devpilot/completions/inline/AcceptDevPilotInlineCompletionAction.java new file mode 100644 index 00000000..16d3ca20 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/AcceptDevPilotInlineCompletionAction.java @@ -0,0 +1,28 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.codeInsight.hint.HintManagerImpl.ActionToIgnore; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.actionSystem.EditorAction; +import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler; + +public class AcceptDevPilotInlineCompletionAction extends EditorAction implements ActionToIgnore, InlineCompletionAction { + public static final String ACTION_ID = "AcceptDevPilotInlineCompletionAction"; + + public AcceptDevPilotInlineCompletionAction() { + super(new AcceptInlineCompletionHandler()); + } + + private static class AcceptInlineCompletionHandler extends EditorWriteActionHandler { + @Override + public void executeWriteAction(Editor editor, Caret caret, DataContext dataContext) { + CompletionPreview.getInstance(editor).applyPreview(caret != null ? caret : editor.getCaretModel().getCurrentCaret()); + } + + @Override + protected boolean isEnabledForCaret(Editor editor, Caret caret, DataContext dataContext) { + return CompletionPreview.getInstance(editor) != null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/CompletionAdjustment.java b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionAdjustment.java new file mode 100644 index 00000000..975a8a33 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionAdjustment.java @@ -0,0 +1,29 @@ +package com.zhongan.devpilot.completions.inline; + +import com.zhongan.devpilot.completions.general.SuggestionTrigger; +import com.zhongan.devpilot.completions.requests.AutocompleteRequest; +import com.zhongan.devpilot.completions.requests.AutocompleteResponse; + +public abstract class CompletionAdjustment { + private boolean cachedOnly = false; + + public abstract SuggestionTrigger getSuggestionTrigger(); + + public CompletionAdjustment withCachedOnly() { + cachedOnly = true; + return this; + } + + public AutocompleteRequest adjustRequest(AutocompleteRequest request) { + request.setCachedOnly(cachedOnly); + return adjustRequestInner(request); + } + + protected AutocompleteRequest adjustRequestInner(AutocompleteRequest autocompleteRequest) { + return autocompleteRequest; + } + + public AutocompleteResponse adjustResponse(AutocompleteResponse autocompleteResponse) { + return autocompleteResponse; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreview.java b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreview.java new file mode 100644 index 00000000..c443c5ae --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreview.java @@ -0,0 +1,183 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.impl.EditorImpl; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.refactoring.rename.inplace.InplaceRefactoring; +import com.zhongan.devpilot.completions.autoimport.handler.AutoImportHandler; +import com.zhongan.devpilot.completions.inline.listeners.InlineCaretListener; +import com.zhongan.devpilot.completions.inline.render.DevPilotInlay; +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; +import com.zhongan.devpilot.util.TelemetryUtils; + +import java.util.List; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.zhongan.devpilot.completions.inline.CompletionPreviewUtils.shouldRemoveSuffix; + +public class CompletionPreview implements Disposable { + private static final Key INLINE_COMPLETION_PREVIEW = + Key.create("INLINE_COMPLETION_PREVIEW"); + + public final Editor editor; + + private final int offset; + + private DevPilotInlay devPilotInlay; + + private List completions; + + private int currentIndex = 0; + + private InlineCaretListener inlineCaretListener; + + private CompletionPreview( + @NotNull Editor editor, List completions, int offset) { + this.editor = editor; + this.completions = completions; + this.offset = offset; + EditorUtil.disposeWithEditor(editor, this); + + this.inlineCaretListener = new InlineCaretListener(this); + + devPilotInlay = DevPilotInlay.create(this); + } + + public static DevPilotCompletion createInstance( + Editor editor, List completions, int offset) { + CompletionPreview preview = getInstance(editor); + + if (preview != null) { + Disposer.dispose(preview); + } + + preview = new CompletionPreview(editor, completions, offset); + + editor.putUserData(INLINE_COMPLETION_PREVIEW, preview); + + return preview.createPreview(); + } + + @Nullable + public static DevPilotCompletion getCurrentCompletion(Editor editor) { + CompletionPreview preview = getInstance(editor); + if (preview == null) return null; + + return preview.getCurrentCompletion(); + } + + @Nullable + public static CompletionPreview getInstance(@NotNull Editor editor) { + return editor.getUserData(INLINE_COMPLETION_PREVIEW); + } + + public static void clear(@NotNull Editor editor) { + CompletionPreview completionPreview = getInstance(editor); + if (completionPreview != null) { + Disposer.dispose(completionPreview); + } + } + +/* public void togglePreview(CompletionOrder order) { + int nextIndex = currentIndex + order.diff(); + currentIndex = (completions.size() + nextIndex) % completions.size(); + + Disposer.dispose(devPilotInlay); + devPilotInlay = DevPilotInlay.create(this); + + createPreview(); + }*/ + + public DevPilotCompletion getCurrentCompletion() { + return completions.get(currentIndex); + } + + private DevPilotCompletion createPreview() { + DevPilotCompletion completion = completions.get(currentIndex); + + if (!(editor instanceof EditorImpl) + || editor.getSelectionModel().hasSelection() + || InplaceRefactoring.getActiveInplaceRenamer(editor) != null) { + return null; + } + + try { + editor.getDocument().startGuardedBlockChecking(); + devPilotInlay.render(this.editor, completion, offset); + return completion; + } finally { + editor.getDocument().stopGuardedBlockChecking(); + } + } + + public void dispose() { + editor.putUserData(INLINE_COMPLETION_PREVIEW, null); + } + + public void applyPreview(@Nullable Caret caret) { + if (caret == null) { + return; + } + + Project project = editor.getProject(); + + if (project == null) { + return; + } + + PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); + + if (file == null) { + return; + } + + try { + applyPreviewInternal(caret.getOffset(), project, file); + } catch (Throwable e) { + Logger.getInstance(getClass()).warn("Failed in the processes of accepting completion", e); + } finally { + Disposer.dispose(this); + } + } + + private void applyPreviewInternal(@NotNull Integer cursorOffset, Project project, PsiFile file) { + CompletionPreview.clear(editor); + DevPilotCompletion completion = completions.get(currentIndex); + String suffix = completion.getSuffix(); + int startOffset = cursorOffset - completion.oldPrefix.length(); + int endOffset = cursorOffset + suffix.length(); + + if (shouldRemoveSuffix(completion)) { + editor.getDocument().deleteString(cursorOffset, cursorOffset + completion.oldSuffix.length()); + } + + //TODO 代码自动格式化 + editor.getDocument().insertString(cursorOffset, suffix); + editor.getCaretModel().moveToOffset(startOffset + completion.newPrefix.length()); + + PsiDocumentManager.getInstance(project).commitAllDocuments(); + + PsiFile fileAfterCompletion = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument()); + + ApplicationManager.getApplication().executeOnPooledThread(() -> { + getAutoImportHandler(editor, fileAfterCompletion, startOffset, endOffset).invoke(); + }); + + TelemetryUtils.completionAccept(completion.id, file); + } + + private static AutoImportHandler getAutoImportHandler(Editor editor, PsiFile file, int startOffset, int endOffset) { + return new AutoImportHandler(startOffset, endOffset, editor, file); + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreviewInsertionHint.java b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreviewInsertionHint.java new file mode 100644 index 00000000..678378f8 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreviewInsertionHint.java @@ -0,0 +1,86 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.event.EditorMouseEvent; +import com.intellij.openapi.editor.event.EditorMouseEventArea; +import com.intellij.openapi.editor.event.EditorMouseMotionListener; +import com.intellij.openapi.util.Disposer; +import com.zhongan.devpilot.completions.inline.render.DevPilotInlay; + +import java.awt.Component; +import java.awt.Point; +import java.awt.Rectangle; + +import javax.swing.SwingUtilities; + +public class CompletionPreviewInsertionHint implements Disposable, EditorMouseMotionListener { + private Editor editor; + + private DevPilotInlay inlay; + + private String suffix; + + public CompletionPreviewInsertionHint(Editor editor, DevPilotInlay inlay, String suffix) { + this.editor = editor; + this.inlay = inlay; + this.suffix = suffix; + editor.addEditorMouseMotionListener(this); + Disposer.register(inlay, this); + } + + @Override + public void mouseMoved(EditorMouseEvent e) { + if (inlay.isEmpty() || e.getArea() != EditorMouseEventArea.EDITING_AREA) { + return; + } + + java.awt.event.MouseEvent mouseEvent = e.getMouseEvent(); + Point point = mouseEvent.getPoint(); + + if (!isOverPreview(point)) { + return; + } + + InlineKeybindingHintUtil.createAndShowHint( + editor, + SwingUtilities.convertPoint( + (Component) mouseEvent.getSource(), + point, + editor.getComponent().getRootPane().getLayeredPane() + ) + ); + } + + @Override + public void dispose() { + editor.removeEditorMouseMotionListener(this); + } + + private boolean isOverPreview(Point p) { + Rectangle bounds = inlay.getBounds(); + if (bounds != null) { + return bounds.contains(p); + } else { + return isLogicallyInsideInlay(p); + } + } + + private boolean isLogicallyInsideInlay(Point p) { + LogicalPosition pos = editor.xyToLogicalPosition(p); + + if (pos.line >= editor.getDocument().getLineCount()) { + return false; + } + + int pointOffset = editor.logicalPositionToOffset(pos); + Integer inlayOffset = inlay.getOffset(); + + if (inlayOffset != null) { + return pointOffset >= inlayOffset && pointOffset <= inlayOffset + suffix.length(); + } + + return false; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreviewUtils.java b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreviewUtils.java new file mode 100644 index 00000000..b6f57b36 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionPreviewUtils.java @@ -0,0 +1,17 @@ +package com.zhongan.devpilot.completions.inline; + +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; + +public class CompletionPreviewUtils { + public static boolean hadSuffix(DevPilotCompletion currentCompletion) { + return currentCompletion.getOldSuffix().trim().length() > 0; + } + + public static boolean isSingleLine(DevPilotCompletion currentCompletion) { + return !currentCompletion.getSuffix().trim().contains("\n"); + } + + public static boolean shouldRemoveSuffix(DevPilotCompletion currentCompletion) { + return hadSuffix(currentCompletion) && isSingleLine(currentCompletion); + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/CompletionTracker.java b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionTracker.java new file mode 100644 index 00000000..be3dbfd0 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionTracker.java @@ -0,0 +1,31 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.Key; +import com.zhongan.devpilot.completions.general.SuggestionTrigger; + +public class CompletionTracker { + private static final Key LAST_COMPLETION_REQUEST_TIME = Key.create("LAST_COMPLETION_REQUEST_TIME"); + + private static final long DEBOUNCE_INTERVAL_MS = DebounceUtils.getDebounceInterval(); + + public static long calcDebounceTimeMs(Editor editor, CompletionAdjustment completionAdjustment) { + if (completionAdjustment.getSuggestionTrigger() == SuggestionTrigger.LookAhead) { + return 0; + } + + Long lastCompletionTimestamp = LAST_COMPLETION_REQUEST_TIME.get(editor); + if (lastCompletionTimestamp != null) { + long elapsedTimeFromLastEvent = System.currentTimeMillis() - lastCompletionTimestamp; + if (elapsedTimeFromLastEvent < DEBOUNCE_INTERVAL_MS) { + return DEBOUNCE_INTERVAL_MS - elapsedTimeFromLastEvent; + } + } + return 0; + } + + public static void updateLastCompletionRequestTime(Editor editor) { + long currentTimestamp = System.currentTimeMillis(); + LAST_COMPLETION_REQUEST_TIME.set(editor, currentTimestamp); + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/CompletionUtils.java b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionUtils.java new file mode 100644 index 00000000..0ff8bfdd --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/CompletionUtils.java @@ -0,0 +1,136 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.TextRange; +import com.zhongan.devpilot.util.CommentUtil; + +import java.util.regex.Pattern; + +import org.apache.commons.lang3.StringUtils; + +public class CompletionUtils { + private static final Pattern END_OF_LINE_VALID_PATTERN = Pattern.compile("^\\s*[)}\\]\"'`]*\\s*[:{;,]?\\s*$"); + + public static boolean isValidDocumentChange(Document document, int newOffset, int previousOffset) { + if (newOffset < 0 || previousOffset > newOffset) return false; + + String addedText = document.getText(new TextRange(previousOffset, newOffset)); + return + isValidMidlinePosition(document, newOffset) && + isValidNonEmptyChange(addedText.length(), addedText) && + isSingleCharNonWhitespaceChange(addedText); + } + + public static boolean isValidMidlinePosition(Document document, int offset) { + int lineIndex = document.getLineNumber(offset); + TextRange lineRange = TextRange.create(document.getLineStartOffset(lineIndex), document.getLineEndOffset(lineIndex)); + String line = document.getText(lineRange); + String lineSuffix = line.substring(offset - lineRange.getStartOffset()); + return END_OF_LINE_VALID_PATTERN.matcher(lineSuffix).matches(); + } + + public static boolean isValidNonEmptyChange(int replacedTextLength, String newText) { + return replacedTextLength >= 0 && !newText.isEmpty(); + } + + public static boolean isSingleCharNonWhitespaceChange(String newText) { + return newText.trim().length() <= 1; + } + + // 代码-单行仅空字符或者仅换行忽略请求,注释-单行除换行全部忽略请求 + public static VerifyResult ignoreTrigger(String newText, String currentLineText) { + boolean isPreComment = CommentUtil.containsComment(currentLineText); + + // only contains empty and tab + boolean emptyAndTabChar = StringUtils.isEmpty(StringUtils.trim(newText)); + boolean currentLineEmpty = StringUtils.isEmpty(StringUtils.trim(currentLineText)); + if (emptyAndTabChar && currentLineEmpty) { + return VerifyResult.create(true); + } + + boolean newlineChar = StringUtils.startsWith(newText, "\n"); + if (newlineChar && isPreComment) { + return VerifyResult.createComment(false); + } + + if (newlineChar || isPreComment) { + return VerifyResult.create(true); + } + + return VerifyResult.create(false); + } + + public static VerifyResult isValidChange(Editor editor, Document document, int newOffset, int previousOffset) { + if (newOffset < 0 || previousOffset > newOffset) return VerifyResult.create(false); + String addedText = document.getText(new TextRange(previousOffset, newOffset)); + int currentLine = editor.getCaretModel().getLogicalPosition().line; + String currentLineText = currentLine < 0 ? null : document.getText( + new TextRange(document.getLineStartOffset(currentLine), document.getLineEndOffset(currentLine))); + + VerifyResult result = ignoreTrigger(addedText, currentLineText); + + boolean valid = isValidMidlinePosition(document, newOffset) && + isValidNonEmptyChange(addedText.length(), addedText) && + isSingleCharNonWhitespaceChange(addedText) && + !result.isValid(); + + return VerifyResult.create(valid, result.getCompletionType()); + } + + public static class VerifyResult { + private boolean valid; + + // default is inline + // type: comment, inline + private String completionType = "inline"; + + public VerifyResult(boolean valid) { + this.valid = valid; + } + + public VerifyResult(boolean valid, String completionType) { + this.valid = valid; + this.completionType = completionType; + } + + public boolean isValid() { + return valid; + } + + public void setValid(boolean valid) { + this.valid = valid; + } + + public String getCompletionType() { + return completionType; + } + + public void setCompletionType(String completionType) { + this.completionType = completionType; + } + + public static VerifyResult create(boolean valid) { + return new VerifyResult(valid); + } + + public static VerifyResult create(boolean valid, String completionType) { + return new VerifyResult(valid, completionType); + } + + public static VerifyResult createComment(boolean valid) { + return new VerifyResult(valid, "comment"); + } + } +} + + + + + + + + + + + diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/DebounceUtils.java b/src/main/java/com/zhongan/devpilot/completions/inline/DebounceUtils.java new file mode 100644 index 00000000..0a952463 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/DebounceUtils.java @@ -0,0 +1,18 @@ +package com.zhongan.devpilot.completions.inline; + +import com.zhongan.devpilot.completions.general.StaticConfig; + +public class DebounceUtils { + + public static long getDebounceInterval() { + //TODO 读取应用配置 +// return getDebounceMsFromCapabilities() ?: AppSettingsState.instance.debounceTime + Long debounceMsFromCapabilities = getDebounceMsFromCapabilities(); + return debounceMsFromCapabilities != null ? debounceMsFromCapabilities : StaticConfig.DEBOUNCE_VALUE_1200; + } + + public static Long getDebounceMsFromCapabilities() { + return StaticConfig.DEBOUNCE_VALUE_300; + } + +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/DefaultCompletionAdjustment.java b/src/main/java/com/zhongan/devpilot/completions/inline/DefaultCompletionAdjustment.java new file mode 100644 index 00000000..7e43e226 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/DefaultCompletionAdjustment.java @@ -0,0 +1,11 @@ +package com.zhongan.devpilot.completions.inline; + +import com.zhongan.devpilot.completions.general.SuggestionTrigger; + +public class DefaultCompletionAdjustment extends CompletionAdjustment { + + @Override + public SuggestionTrigger getSuggestionTrigger() { + return SuggestionTrigger.DocumentChanged; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/DevPilotDocumentListener.java b/src/main/java/com/zhongan/devpilot/completions/inline/DevPilotDocumentListener.java new file mode 100644 index 00000000..abf49ed7 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/DevPilotDocumentListener.java @@ -0,0 +1,110 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.ide.DataManager; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorKind; +import com.intellij.openapi.editor.event.BulkAwareDocumentListener; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.wm.IdeFocusManager; +import com.zhongan.devpilot.completions.general.EditorUtils; +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; +import com.zhongan.devpilot.settings.state.CompletionSettingsState; + +import java.awt.Component; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.intellij.openapi.editor.EditorModificationUtil.checkModificationAllowed; +import static com.zhongan.devpilot.completions.general.DependencyContainer.singletonOfInlineCompletionHandler; + +public class DevPilotDocumentListener implements BulkAwareDocumentListener { + private final InlineCompletionHandler handler = singletonOfInlineCompletionHandler(); + + @Nullable + private static Editor getActiveEditor(@NotNull Document document) { + if (!ApplicationManager.getApplication().isDispatchThread()) { + return null; + } + + Component focusOwner = IdeFocusManager.getGlobalInstance().getFocusOwner(); + DataContext dataContext = DataManager.getInstance().getDataContext(focusOwner); + // ignore caret placing when exiting + Editor activeEditor = + ApplicationManager.getApplication().isDisposed() + ? null + : CommonDataKeys.EDITOR.getData(dataContext); + + if (activeEditor != null && activeEditor.getDocument() != document) { + activeEditor = null; + } + + return activeEditor; + } + + @Override + public void documentChangedNonBulk(@NotNull DocumentEvent event) { + if (!CompletionSettingsState.getInstance().getEnable()) { + return; + } + Document document = event.getDocument(); + Editor editor = getActiveEditor(document); + if (editor == null || !EditorUtils.isMainEditor(editor)) { + return; + } + DevPilotCompletion lastShownCompletion = CompletionPreview.getCurrentCompletion(editor); + CompletionPreview.clear(editor); + int offset = event.getOffset() + event.getNewLength(); + + CompletionUtils.VerifyResult result = shouldIgnoreChange(event, editor, offset, lastShownCompletion); + + if (result.isValid()) { + InlineCompletionCache.INSTANCE.clear(editor); + return; + } + + handler.retrieveAndShowCompletion( + editor, + offset, + lastShownCompletion, + event.getNewFragment().toString(), + new DefaultCompletionAdjustment(), + result.getCompletionType()); + + } + + private CompletionUtils.VerifyResult shouldIgnoreChange( + DocumentEvent event, Editor editor, int offset, DevPilotCompletion lastShownCompletion) { + Document document = event.getDocument(); + +// if (!suggestionsModeService.getSuggestionMode().isInlineEnabled()) { +// return true; +// } + + if (event.getNewLength() < 1) { + return CompletionUtils.VerifyResult.create(true); + } + + if (!editor.getEditorKind().equals(EditorKind.MAIN_EDITOR) + && !ApplicationManager.getApplication().isUnitTestMode()) { + return CompletionUtils.VerifyResult.create(true); + } + + if (!checkModificationAllowed(editor) || document.getRangeGuard(offset, offset) != null) { + document.fireReadOnlyModificationAttempt(); + return CompletionUtils.VerifyResult.create(true); + } + +// return !CompletionUtils.isValidDocumentChange(document, offset, event.getOffset()); + + CompletionUtils.VerifyResult result = CompletionUtils + .isValidChange(editor, document, offset, event.getOffset()); + + return CompletionUtils.VerifyResult.create(!result.isValid(), result.getCompletionType()); + } + +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/EscapeHandler.java b/src/main/java/com/zhongan/devpilot/completions/inline/EscapeHandler.java new file mode 100644 index 00000000..1043cf96 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/EscapeHandler.java @@ -0,0 +1,30 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Caret; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.actionSystem.EditorActionHandler; + +public class EscapeHandler extends EditorActionHandler { + public static final String ACTION_ID = "EditorEscape"; + + private final EditorActionHandler myOriginalHandler; + + public EscapeHandler(EditorActionHandler originalHandler) { + myOriginalHandler = originalHandler; + } + + @Override + public void doExecute(Editor editor, Caret caret, DataContext dataContext) { + CompletionPreview.clear(editor); + if (myOriginalHandler.isEnabled(editor, caret, dataContext)) { + myOriginalHandler.execute(editor, caret, dataContext); + } + } + + @Override + public boolean isEnabledForCaret(Editor editor, Caret caret, DataContext dataContext) { + CompletionPreview preview = CompletionPreview.getInstance(editor); + return preview != null || myOriginalHandler.isEnabled(editor, caret, dataContext); + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/InlineActionsPromoter.java b/src/main/java/com/zhongan/devpilot/completions/inline/InlineActionsPromoter.java new file mode 100644 index 00000000..db1049cc --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/InlineActionsPromoter.java @@ -0,0 +1,31 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.openapi.actionSystem.ActionPromoter; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.editor.Editor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.jetbrains.annotations.NotNull; + +public class InlineActionsPromoter implements ActionPromoter { + + @Override + public List promote(@NotNull List actions, @NotNull DataContext context) { + Editor editor = CommonDataKeys.EDITOR.getData(context); + if (editor != null) { + CompletionPreview preview = CompletionPreview.getInstance(editor); + if (preview != null) { + return actions.stream() + .filter(action -> action instanceof InlineCompletionAction) + .collect(Collectors.toList()); + } + } + return Collections.emptyList(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionAction.java b/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionAction.java new file mode 100644 index 00000000..3b88f09d --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionAction.java @@ -0,0 +1,7 @@ +package com.zhongan.devpilot.completions.inline; + +/** + * Marker for Inline Completion actions. + */ +public interface InlineCompletionAction { +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionCache.java b/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionCache.java new file mode 100644 index 00000000..1adce123 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionCache.java @@ -0,0 +1,39 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.Key; +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class InlineCompletionCache { + public static final InlineCompletionCache INSTANCE = new InlineCompletionCache(); + + private static final Key> INLINE_COMPLETIONS_LAST_RESULT = Key.create("INLINE_COMPLETIONS_LAST_RESULT"); + + private InlineCompletionCache() { + } + + public static void store(Editor editor, List completions) { + editor.putUserData(INLINE_COMPLETIONS_LAST_RESULT, completions); + } + + public static void clear(Editor editor) { + editor.putUserData(INLINE_COMPLETIONS_LAST_RESULT, null); + } + + public List retrieveAdjustedCompletions(Editor editor, String userInput) { + List completions = editor.getUserData(INLINE_COMPLETIONS_LAST_RESULT); + if (completions == null) { + return Collections.emptyList(); + } + return completions.stream() + .filter(completion -> completion.getSuffix().startsWith(userInput)) + .map(completion -> completion.createAdjustedCompletion( + completion.getOldPrefix() + userInput, + completion.getCursorPrefix() + userInput)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionHandler.java b/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionHandler.java new file mode 100644 index 00000000..a2b7786d --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/InlineCompletionHandler.java @@ -0,0 +1,269 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.util.ObjectUtils; +import com.zhongan.devpilot.completions.CompletionUtils; +import com.zhongan.devpilot.completions.general.CompletionKind; +import com.zhongan.devpilot.completions.general.SuggestionTrigger; +import com.zhongan.devpilot.completions.general.Utils; +import com.zhongan.devpilot.completions.inline.render.GraphicsUtils; +import com.zhongan.devpilot.completions.prediction.CompletionFacade; +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; +import com.zhongan.devpilot.completions.requests.AutocompleteResponse; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class InlineCompletionHandler { + private final CompletionFacade completionFacade; + + private Future lastDebounceRenderTask = null; + + private Future lastFetchAndRenderTask = null; + + private Future lastFetchInBackgroundTask = null; + + public InlineCompletionHandler( + CompletionFacade completionFacade + ) { + this.completionFacade = completionFacade; + } + + public void retrieveAndShowCompletion( + @NotNull Editor editor, + int offset, + @Nullable DevPilotCompletion lastShownSuggestion, + @NotNull String userInput, + @NotNull CompletionAdjustment completionAdjustment, + String completionType) { + Integer tabSize = GraphicsUtils.getTabSize(editor); + + ObjectUtils.doIfNotNull(lastFetchInBackgroundTask, task -> task.cancel(false)); + ObjectUtils.doIfNotNull(lastFetchAndRenderTask, task -> task.cancel(false)); + ObjectUtils.doIfNotNull(lastDebounceRenderTask, task -> task.cancel(false)); + + List cachedCompletions = + InlineCompletionCache.INSTANCE.retrieveAdjustedCompletions(editor, userInput); + if (!cachedCompletions.isEmpty()) { + renderCachedCompletions(editor, offset, tabSize, cachedCompletions, completionAdjustment, completionType); + return; + } + + ApplicationManager.getApplication() + .invokeLater( + () -> + renderNewCompletions( + editor, + tabSize, + getCurrentEditorOffset(editor, userInput), + editor.getDocument().getModificationStamp(), + completionAdjustment, + completionType)); + } + + private void renderCachedCompletions( + @NotNull Editor editor, + int offset, + Integer tabSize, + @NotNull List cachedCompletions, + @NotNull CompletionAdjustment completionAdjustment, + String completionType) { + showInlineCompletion(editor, cachedCompletions, offset, null); + lastFetchInBackgroundTask = + Utils.executeThread( + () -> retrieveInlineCompletion(editor, offset, tabSize, completionAdjustment, completionType)); + } + + private int getCurrentEditorOffset(@NotNull Editor editor, @NotNull String userInput) { + return editor.getCaretModel().getOffset() + + (ApplicationManager.getApplication().isUnitTestMode() ? userInput.length() : 0); + } + + private void renderNewCompletions( + @NotNull Editor editor, + Integer tabSize, + int offset, + long modificationStamp, + @NotNull CompletionAdjustment completionAdjustment, + String completionType) { + lastFetchAndRenderTask = + Utils.executeThread( + () -> { + CompletionTracker.updateLastCompletionRequestTime(editor); + long debounceTimeMs = CompletionTracker.calcDebounceTimeMs(editor, completionAdjustment); + if (debounceTimeMs == 0) { + debounceTimeMs = logAndGetEmptySuggestionsDebounceMillis(); + } + refetchCompletionsAfterDebounce( + editor, tabSize, offset, modificationStamp, completionAdjustment, debounceTimeMs, completionType); + }); + } + + private int logAndGetEmptySuggestionsDebounceMillis() { + int debounceMillis = 300; + Logger.getInstance(getClass()) + .info( + String.format( + "Got empty suggestions, waiting %s ms before retrying to fetch", debounceMillis)); + return debounceMillis; + } + + private void refetchCompletionsAfterDebounce( + @NotNull Editor editor, + Integer tabSize, + int offset, + long modificationStamp, + @NotNull CompletionAdjustment completionAdjustment, + long debounceTime, + String completionType) { + lastDebounceRenderTask = + Utils.executeThread( + () -> { + CompletionAdjustment cachedOnlyCompletionAdjustment = + completionAdjustment.withCachedOnly(); + List completions = + retrieveInlineCompletion(editor, offset, tabSize, cachedOnlyCompletionAdjustment, completionType); + rerenderCompletion( + editor, completions, offset, modificationStamp, cachedOnlyCompletionAdjustment); + }, + debounceTime, + TimeUnit.MILLISECONDS); + } + + private void rerenderCompletion( + @NotNull Editor editor, + List completions, + int offset, + long modificationStamp, + @NotNull CompletionAdjustment completionAdjustment) { + ApplicationManager.getApplication() + .invokeLater( + () -> { + if (shouldCancelRendering(editor, modificationStamp, offset)) { + return; + } +/* if (shouldRemovePopupCompletions(completionAdjustment)) { + completions.removeIf(completion -> !completion.isSnippet()); + }*/ + showInlineCompletion( + editor, + completions, + offset, + (completion) -> afterCompletionShown(completion, editor)); + }); + } + + private boolean shouldCancelRendering( + @NotNull Editor editor, long modificationStamp, int offset) { + if (Utils.isUnitTestMode()) { + return false; + } + boolean isModificationStampChanged = + modificationStamp != editor.getDocument().getModificationStamp(); + boolean isOffsetChanged = offset != editor.getCaretModel().getOffset(); + return isModificationStampChanged || isOffsetChanged; + } + + private List retrieveInlineCompletion( + @NotNull Editor editor, + int offset, + Integer tabSize, + @NotNull CompletionAdjustment completionAdjustment, + String completionType) { + AutocompleteResponse completionsResponse = + this.completionFacade.retrieveCompletions(editor, offset, tabSize, completionAdjustment, completionType); + + if (completionsResponse == null || completionsResponse.results.length == 0) { + return Collections.emptyList(); + } + + return createCompletions( + completionsResponse, + editor.getDocument(), + offset, + completionAdjustment.getSuggestionTrigger()); + } + + private void showInlineCompletion( + @NotNull Editor editor, + List completions, + int offset, + @Nullable OnCompletionPreviewUpdatedCallback onCompletionPreviewUpdatedCallback) { + if (completions.isEmpty()) { + return; + } + InlineCompletionCache.INSTANCE.store(editor, completions); + + DevPilotCompletion displayedCompletion = + CompletionPreview.createInstance(editor, completions, offset); + + if (displayedCompletion == null) { + return; + } + + if (onCompletionPreviewUpdatedCallback != null) { + onCompletionPreviewUpdatedCallback.onCompletionPreviewUpdated(displayedCompletion); + } + } + + private void afterCompletionShown(DevPilotCompletion completion, Editor editor) { + if (completion.completionMetadata == null) return; + Boolean isCached = completion.completionMetadata.getIsCached(); + + try { + String filename = + CompletionFacade.getFilename(FileDocumentManager.getInstance().getFile(editor.getDocument())); + if (filename == null) { + Logger.getInstance(getClass()) + .warn("Could not send SuggestionShown request. the filename is null"); + return; + } + //TODO 调用openAI + + if (completion.completionMetadata.getCompletionKind() == CompletionKind.Snippet + && !isCached) { + Map context = completion.completionMetadata.getSnippetContext(); + if (context == null) { + Logger.getInstance(getClass()) + .warn("Could not send SnippetShown request. intent is null"); + return; + } + + //TODO 调用openAI + } + } catch (RuntimeException e) { + // swallow - nothing to do with this + } + } + + private List createCompletions( + AutocompleteResponse completions, + @NotNull Document document, + int offset, + SuggestionTrigger suggestionTrigger) { + return IntStream.range(0, completions.results.length) + .mapToObj( + index -> + CompletionUtils.createDevpilotCompletion( + document, + offset, + completions.oldPrefix, + completions.results[index], + index, + suggestionTrigger)) + .filter(completion -> completion != null && !completion.getSuffix().isEmpty()) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/InlineKeybindingHintComponent.java b/src/main/java/com/zhongan/devpilot/completions/inline/InlineKeybindingHintComponent.java new file mode 100644 index 00000000..a95cd889 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/InlineKeybindingHintComponent.java @@ -0,0 +1,20 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.ui.SimpleColoredComponent; +import com.intellij.util.ui.JBUI; + +import java.awt.BorderLayout; + +import javax.swing.JPanel; + +public class InlineKeybindingHintComponent extends JPanel { + public InlineKeybindingHintComponent(SimpleColoredComponent component) { + setLayout(new BorderLayout()); + setBorder(JBUI.Borders.empty()); + add(component, BorderLayout.CENTER); + setOpaque(true); + setBackground(component.getBackground()); + revalidate(); + repaint(); + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/InlineKeybindingHintUtil.java b/src/main/java/com/zhongan/devpilot/completions/inline/InlineKeybindingHintUtil.java new file mode 100644 index 00000000..13b57870 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/InlineKeybindingHintUtil.java @@ -0,0 +1,67 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.codeInsight.hint.HintManager; +import com.intellij.codeInsight.hint.HintManagerImpl; +import com.intellij.codeInsight.hint.HintUtil; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.keymap.KeymapUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.ui.LightweightHint; +import com.intellij.ui.SimpleColoredComponent; +import com.intellij.ui.SimpleColoredText; +import com.intellij.ui.SimpleTextAttributes; + +import java.awt.Point; +import java.awt.event.KeyEvent; + +import javax.swing.JComponent; + +import org.jetbrains.annotations.NotNull; + +import static com.zhongan.devpilot.DevPilotIcons.SYSTEM_ICON; + +public class InlineKeybindingHintUtil { + public static void createAndShowHint(@NotNull Editor editor, @NotNull Point pos) { + try { + HintManagerImpl.getInstanceImpl() + .showEditorHint( + new LightweightHint(createInlineHintComponent()), + editor, + pos, + HintManager.HIDE_BY_ANY_KEY | HintManager.UPDATE_BY_SCROLLING, + 0, + false); + } catch (Throwable e) { + Logger.getInstance(InlineKeybindingHintUtil.class) + .warn("Failed to show inline key bindings hints", e); + } + } + + private static JComponent createInlineHintComponent() { + SimpleColoredComponent component = HintUtil.createInformationComponent(); + + component.setIconOnTheRight(true); + component.setIcon(SYSTEM_ICON); + + SimpleColoredText coloredText = + new SimpleColoredText(hintText(), SimpleTextAttributes.REGULAR_ATTRIBUTES); + + coloredText.appendToComponent(component); + + return new InlineKeybindingHintComponent(component); + } + + private static String hintText() { + String acceptShortcut = getShortcutText(AcceptDevPilotInlineCompletionAction.ACTION_ID); + String cancelShortcut = KeymapUtil.getKeyText(KeyEvent.VK_ESCAPE); + return String.format("Accept (%s) Cancel (%s)", acceptShortcut, cancelShortcut); + } + + private static String getShortcutText(String actionId) { + return StringUtil.defaultIfEmpty( + KeymapUtil.getFirstKeyboardShortcutText(ActionManager.getInstance().getAction(actionId)), + "Missing shortcut key"); + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/ManualTriggerDevPilotInlineCompletionAction.java b/src/main/java/com/zhongan/devpilot/completions/inline/ManualTriggerDevPilotInlineCompletionAction.java new file mode 100644 index 00000000..22b812b9 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/ManualTriggerDevPilotInlineCompletionAction.java @@ -0,0 +1,44 @@ +package com.zhongan.devpilot.completions.inline; + +import com.intellij.codeInsight.CodeInsightActionHandler; +import com.intellij.codeInsight.actions.BaseCodeInsightAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiFile; +import com.zhongan.devpilot.completions.general.DependencyContainer; +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; + +import org.jetbrains.annotations.NotNull; + +public class ManualTriggerDevPilotInlineCompletionAction extends BaseCodeInsightAction implements InlineCompletionAction { + + public static final String ACTION_ID = "ManualTriggerDevPilotInlineCompletionAction"; + + private final InlineCompletionHandler handler = DependencyContainer.singletonOfInlineCompletionHandler(); + + @NotNull + @Override + protected CodeInsightActionHandler getHandler() { + return new CodeInsightActionHandler() { + @Override + public void invoke(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile psiFile) { + DevPilotCompletion lastShownCompletion = CompletionPreview.getCurrentCompletion(editor); + handler.retrieveAndShowCompletion( + editor, editor.getCaretModel().getOffset(), lastShownCompletion, "", + new DefaultCompletionAdjustment(), null + ); + } + + @Override + public boolean startInWriteAction() { + return false; + } + }; + } + + @Override + protected boolean isValidForLookup() { + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/OnCompletionPreviewUpdatedCallback.java b/src/main/java/com/zhongan/devpilot/completions/inline/OnCompletionPreviewUpdatedCallback.java new file mode 100644 index 00000000..3955152d --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/OnCompletionPreviewUpdatedCallback.java @@ -0,0 +1,7 @@ +package com.zhongan.devpilot.completions.inline; + +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; + +public interface OnCompletionPreviewUpdatedCallback { + void onCompletionPreviewUpdated(DevPilotCompletion completion); +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/listeners/InlineCaretListener.java b/src/main/java/com/zhongan/devpilot/completions/inline/listeners/InlineCaretListener.java new file mode 100644 index 00000000..2ee2d9d6 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/listeners/InlineCaretListener.java @@ -0,0 +1,40 @@ +package com.zhongan.devpilot.completions.inline.listeners; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.editor.event.CaretEvent; +import com.intellij.openapi.editor.event.CaretListener; +import com.intellij.openapi.util.Disposer; +import com.zhongan.devpilot.completions.inline.CompletionPreview; +import com.zhongan.devpilot.completions.inline.InlineCompletionCache; + +import org.jetbrains.annotations.NotNull; + +public class InlineCaretListener implements CaretListener, Disposable { + private final CompletionPreview completionPreview; + + public InlineCaretListener(CompletionPreview completionPreview) { + this.completionPreview = completionPreview; + Disposer.register(completionPreview, this); + completionPreview.editor.getCaretModel().addCaretListener(this); + } + + @Override + public void caretPositionChanged(@NotNull CaretEvent event) { + if (isSingleOffsetChange(event)) { + return; + } + + Disposer.dispose(completionPreview); + InlineCompletionCache.clear(event.getEditor()); + } + + private boolean isSingleOffsetChange(CaretEvent event) { + return event.getOldPosition().line == event.getNewPosition().line + && event.getOldPosition().column + 1 == event.getNewPosition().column; + } + + @Override + public void dispose() { + completionPreview.editor.getCaretModel().removeCaretListener(this); + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/BlockElementRenderer.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/BlockElementRenderer.java new file mode 100644 index 00000000..785b73b4 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/BlockElementRenderer.java @@ -0,0 +1,62 @@ +package com.zhongan.devpilot.completions.inline.render; + +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorCustomElementRenderer; +import com.intellij.openapi.editor.Inlay; +import com.intellij.openapi.editor.markup.TextAttributes; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.util.List; + +import org.jetbrains.annotations.TestOnly; + +public class BlockElementRenderer implements EditorCustomElementRenderer { + private Editor editor; + + private List blockText; + + private boolean deprecated; + + private Color color; + + public BlockElementRenderer(Editor editor, List blockText, boolean deprecated) { + this.editor = editor; + this.blockText = blockText; + this.deprecated = deprecated; + } + + @Override + public int calcWidthInPixels(Inlay inlay) { + String firstLine = blockText.get(0); + return editor.getContentComponent() + .getFontMetrics(GraphicsUtils.getFont(editor, deprecated)).stringWidth(firstLine); + } + + @Override + public int calcHeightInPixels(Inlay inlay) { + return editor.getLineHeight() * blockText.size(); + } + + @Override + public void paint(Inlay inlay, Graphics g, Rectangle targetRegion, TextAttributes textAttributes) { + color = color != null ? color : GraphicsUtils.getColor(); + g.setColor(color); + g.setFont(GraphicsUtils.getFont(editor, deprecated)); + + for (int i = 0; i < blockText.size(); i++) { + String line = blockText.get(i); + g.drawString( + line, + 0, + targetRegion.y + i * editor.getLineHeight() + editor.getAscent() + ); + } + } + + @TestOnly + public String getContent() { + return String.join("\n", blockText); + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/DefaultDevPilotInlay.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/DefaultDevPilotInlay.java new file mode 100644 index 00000000..d3e7c1e6 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/DefaultDevPilotInlay.java @@ -0,0 +1,147 @@ +package com.zhongan.devpilot.completions.inline.render; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.Inlay; +import com.intellij.openapi.util.Disposer; +import com.zhongan.devpilot.completions.general.Utils; +import com.zhongan.devpilot.completions.inline.CompletionPreviewInsertionHint; +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; + +import java.awt.Rectangle; +import java.util.List; +import java.util.stream.Collectors; + +public class DefaultDevPilotInlay implements DevPilotInlay { + private Inlay beforeSuffixInlay; + + private Inlay afterSuffixInlay; + + private Inlay blockInlay; + + private CompletionPreviewInsertionHint insertionHint; + + public DefaultDevPilotInlay(Disposable parent) { + Disposer.register(parent, this); + } + + @Override + public Integer getOffset() { + return beforeSuffixInlay != null ? beforeSuffixInlay.getOffset() : + afterSuffixInlay != null ? afterSuffixInlay.getOffset() : + blockInlay != null ? blockInlay.getOffset() : null; + } + + @Override + public Rectangle getBounds() { + Rectangle result = beforeSuffixInlay != null ? new Rectangle(beforeSuffixInlay.getBounds()) : null; + + if (result != null) { + Rectangle after = afterSuffixInlay != null ? afterSuffixInlay.getBounds() : null; + Rectangle blockBounds = blockInlay != null ? blockInlay.getBounds() : null; + + if (after != null) { + result.add(after); + } + if (blockBounds != null) { + result.add(blockBounds); + } + } + + return result; + } + + @Override + public Boolean isEmpty() { + return beforeSuffixInlay == null && afterSuffixInlay == null && blockInlay == null; + } + + @Override + public void dispose() { + if (beforeSuffixInlay != null) { + Disposer.dispose(beforeSuffixInlay); + beforeSuffixInlay = null; + } + if (afterSuffixInlay != null) { + Disposer.dispose(afterSuffixInlay); + afterSuffixInlay = null; + } + if (blockInlay != null) { + Disposer.dispose(blockInlay); + blockInlay = null; + } + } + + @Override + public void render(Editor editor, DevPilotCompletion completion, int offset) { + List lines = Utils.asLines(completion.getSuffix()); + if (lines.isEmpty()) return; + String firstLine = lines.get(0); + int endIndex = firstLine.indexOf(completion.getOldSuffix()); + + RenderingInstructions instructions = InlineStringProcessor.determineRendering(lines, completion.getOldSuffix()); + + switch (instructions.getFirstLine()) { + case NoSuffix: + renderNoSuffix(editor, firstLine, completion, offset); + break; + + case SuffixOnly: + renderAfterSuffix(endIndex, completion, firstLine, editor, offset); + break; + + case BeforeAndAfterSuffix: + renderBeforeSuffix(firstLine, endIndex, editor, completion, offset); + renderAfterSuffix(endIndex, completion, firstLine, editor, offset); + break; + + case None: + break; + } + + if (instructions.shouldRenderBlock()) { + List otherLines = lines.stream().skip(1).collect(Collectors.toList()); + renderBlock(otherLines, editor, completion, offset); + } + + if (instructions.getFirstLine() != FirstLineRendering.None) { + insertionHint = new CompletionPreviewInsertionHint(editor, this, completion.getSuffix()); + } + } + + private void renderBlock(List lines, Editor editor, DevPilotCompletion completion, int offset) { + BlockElementRenderer blockElementRenderer = new BlockElementRenderer(editor, lines, completion.getCompletionMetadata() != null ? + completion.getCompletionMetadata().getIsDeprecated() : false); + Inlay element = editor.getInlayModel().addBlockElement(offset, true, false, 1, blockElementRenderer); + if (element != null) { + Disposer.register(this, element); + } + blockInlay = element; + } + + private void renderAfterSuffix(int endIndex, DevPilotCompletion completion, String firstLine, Editor editor, int offset) { + int afterSuffixIndex = endIndex + completion.getOldSuffix().length(); + if (afterSuffixIndex < firstLine.length()) { + afterSuffixInlay = renderInline(editor, firstLine.substring(afterSuffixIndex), completion, offset + completion.getOldSuffix().length()); + } + } + + private void renderBeforeSuffix(String firstLine, int endIndex, Editor editor, DevPilotCompletion completion, int offset) { + String beforeSuffix = firstLine.substring(0, endIndex); + beforeSuffixInlay = renderInline(editor, beforeSuffix, completion, offset); + } + + private void renderNoSuffix(Editor editor, String firstLine, DevPilotCompletion completion, int offset) { + beforeSuffixInlay = renderInline(editor, firstLine, completion, offset); + } + + private Inlay renderInline(Editor editor, String before, DevPilotCompletion completion, int offset) { + InlineElementRenderer element = new InlineElementRenderer(editor, before, completion.getCompletionMetadata() != null ? + completion.getCompletionMetadata().getIsDeprecated() : false); + Inlay inlay = editor.getInlayModel().addInlineElement(offset, true, element); + if (inlay != null) { + Disposer.register(this, inlay); + } + return inlay; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/DevPilotInlay.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/DevPilotInlay.java new file mode 100644 index 00000000..6873dbec --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/DevPilotInlay.java @@ -0,0 +1,21 @@ +package com.zhongan.devpilot.completions.inline.render; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.editor.Editor; +import com.zhongan.devpilot.completions.prediction.DevPilotCompletion; + +import java.awt.Rectangle; + +public interface DevPilotInlay extends Disposable { + static DevPilotInlay create(Disposable parent) { + return new DefaultDevPilotInlay(parent); + } + + Integer getOffset(); + + Boolean isEmpty(); + + Rectangle getBounds(); + + void render(Editor editor, DevPilotCompletion completion, int offset); +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/FirstLineRendering.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/FirstLineRendering.java new file mode 100644 index 00000000..bc38b901 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/FirstLineRendering.java @@ -0,0 +1,8 @@ +package com.zhongan.devpilot.completions.inline.render; + +public enum FirstLineRendering { + None, + NoSuffix, + SuffixOnly, + BeforeAndAfterSuffix +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/GraphicsUtils.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/GraphicsUtils.java new file mode 100644 index 00000000..c1a4a6b3 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/GraphicsUtils.java @@ -0,0 +1,92 @@ +package com.zhongan.devpilot.completions.inline.render; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.colors.EditorFontType; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.codeStyle.CommonCodeStyleSettings; +import com.intellij.ui.JBColor; + +import java.awt.Color; +import java.awt.Font; +import java.awt.font.TextAttribute; +import java.util.HashMap; +import java.util.Map; + +import static java.awt.font.TextAttribute.POSTURE_OBLIQUE; + +public class GraphicsUtils { + public static Font getFont(Editor editor, boolean deprecated) { + Font font = editor.getColorsScheme().getFont(EditorFontType.ITALIC); + + // Check if the font family is Monospaced + if ("JetBrains Mono".equalsIgnoreCase(font.getFamily()) || "Monospaced".equalsIgnoreCase(font.getFamily())) { + // Create a new font with the font attributes, but change the family to DialogInput + Map attributes = new HashMap<>(font.getAttributes()); + attributes.put(TextAttribute.FAMILY, "DialogInput"); + attributes.put(TextAttribute.POSTURE, POSTURE_OBLIQUE); + font = new Font(attributes); + } + + // If the font is not deprecated, return it as it is + if (!deprecated) { + return font; + } + + // Create a new font with the font attributes, but add the strikethrough attribute + Map attributes = new HashMap<>(font.getAttributes()); + attributes.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON); + return new Font(attributes); + } + + public static Color getColor() { + return new Color(niceContrastColor().getRGB()); + } + + public static Color niceContrastColor() { + double averageBrightness = (getBrightness(JBColor.background()) + getBrightness(JBColor.foreground())) / 2.0; + Color currentResult = Color.lightGray; + Color bestResult = currentResult; + double distance = Double.MAX_VALUE; + double currentBrightness = getBrightness(currentResult); + double minBrightness = getBrightness(Color.darkGray); + + while (currentBrightness > minBrightness) { + if (Math.abs(currentBrightness - averageBrightness) < distance) { + distance = Math.abs(currentBrightness - averageBrightness); + bestResult = currentResult; + } + currentResult = currentResult.darker(); + currentBrightness = getBrightness(currentResult); + } + return bestResult; + } + + private static double getBrightness(Color color) { + return Math.sqrt((color.getRed() * color.getRed() * 0.241) + (color.getGreen() * color.getGreen() * 0.691) + (color.getBlue() * color.getBlue() * 0.068)); + } + + public static Integer getTabSize(Editor editor) { + if (!ApplicationManager.getApplication().isReadAccessAllowed()) { + Logger.getInstance("GraphicsUtils").warn("Read access is not allowed here - returning null"); + failIfAlpha(); + return null; + } + CommonCodeStyleSettings commonCodeStyleSettings = editor.getProject() != null ? PsiDocumentManager.getInstance(editor.getProject()).getPsiFile(editor.getDocument()) != null ? new CommonCodeStyleSettings(PsiDocumentManager.getInstance(editor.getProject()).getPsiFile(editor.getDocument()).getLanguage()) : null : null; + + return commonCodeStyleSettings != null && commonCodeStyleSettings.getIndentOptions() != null ? commonCodeStyleSettings.getIndentOptions().TAB_SIZE : editor.getSettings().getTabSize(editor.getProject()); + } + + private static void failIfAlpha() { + boolean isAlpha = true; + boolean isTest = ApplicationManager.getApplication().isUnitTestMode(); + if (isAlpha && !isTest) { + Logger.getInstance("GraphicsUtils").error("!!!Alpha user please notice!!! You called `getTabSize` from a thread without read access. Because you're alpha, a `RuntimeException` will be thrown - This is being done in order to cause chaos for alpha devs, so that they'll fix it."); + throw new RuntimeException("You called `getTabSize` from a thread without read access!"); + } + } + +} + + diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineElementRenderer.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineElementRenderer.java new file mode 100644 index 00000000..f42a39e8 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineElementRenderer.java @@ -0,0 +1,47 @@ +package com.zhongan.devpilot.completions.inline.render; + +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorCustomElementRenderer; +import com.intellij.openapi.editor.Inlay; +import com.intellij.openapi.editor.markup.TextAttributes; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Rectangle; + +import org.jetbrains.annotations.TestOnly; + +public class InlineElementRenderer implements EditorCustomElementRenderer { + private final Editor editor; + + private final String suffix; + + private boolean deprecated; + + private Color color; + + public InlineElementRenderer(Editor editor, String suffix, boolean deprecated) { + this.editor = editor; + this.suffix = suffix; + this.deprecated = deprecated; + } + + @Override + public int calcWidthInPixels(Inlay inlay) { + return editor.getContentComponent() + .getFontMetrics(GraphicsUtils.getFont(editor, deprecated)).stringWidth(suffix); + } + + @TestOnly + public String getContent() { + return suffix; + } + + @Override + public void paint(Inlay inlay, Graphics g, Rectangle targetRegion, TextAttributes textAttributes) { + color = color != null ? color : GraphicsUtils.getColor(); + g.setColor(color); + g.setFont(GraphicsUtils.getFont(editor, deprecated)); + g.drawString(suffix, targetRegion.x, targetRegion.y + editor.getAscent()); + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineStringProcessor.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineStringProcessor.java new file mode 100644 index 00000000..43772f32 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/InlineStringProcessor.java @@ -0,0 +1,31 @@ +package com.zhongan.devpilot.completions.inline.render; + +import java.util.List; + +public class InlineStringProcessor { + + public static RenderingInstructions determineRendering(List textLines, String oldSuffix) { + if (textLines.isEmpty()) { + return new RenderingInstructions(FirstLineRendering.None, false); + } + + boolean shouldRenderBlock = textLines.size() > 1; + + if (!textLines.get(0).trim().isEmpty()) { + if (!oldSuffix.trim().isEmpty()) { + int endIndex = textLines.get(0).indexOf(oldSuffix); + + if (endIndex == 0) { + return new RenderingInstructions(FirstLineRendering.SuffixOnly, shouldRenderBlock); + } else if (endIndex > 0) { + return new RenderingInstructions(FirstLineRendering.BeforeAndAfterSuffix, shouldRenderBlock); + } + } + + return new RenderingInstructions(FirstLineRendering.NoSuffix, shouldRenderBlock); + } + + return new RenderingInstructions(FirstLineRendering.None, shouldRenderBlock); + } + +} diff --git a/src/main/java/com/zhongan/devpilot/completions/inline/render/RenderingInstructions.java b/src/main/java/com/zhongan/devpilot/completions/inline/render/RenderingInstructions.java new file mode 100644 index 00000000..2f350783 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/inline/render/RenderingInstructions.java @@ -0,0 +1,20 @@ +package com.zhongan.devpilot.completions.inline.render; + +public class RenderingInstructions { + private FirstLineRendering firstLine; + + private boolean shouldRenderBlock; + + public RenderingInstructions(FirstLineRendering firstLine, boolean shouldRenderBlock) { + this.firstLine = firstLine; + this.shouldRenderBlock = shouldRenderBlock; + } + + public FirstLineRendering getFirstLine() { + return firstLine; + } + + public boolean shouldRenderBlock() { + return shouldRenderBlock; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/prediction/CompletionFacade.java b/src/main/java/com/zhongan/devpilot/completions/prediction/CompletionFacade.java new file mode 100644 index 00000000..bf5d2789 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/prediction/CompletionFacade.java @@ -0,0 +1,204 @@ +package com.zhongan.devpilot.completions.prediction; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.projectRoots.Sdk; +import com.intellij.openapi.roots.ProjectRootManager; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.ObjectUtils; +import com.zhongan.devpilot.completions.inline.CompletionAdjustment; +import com.zhongan.devpilot.completions.requests.AutocompleteRequest; +import com.zhongan.devpilot.completions.requests.AutocompleteResponse; +import com.zhongan.devpilot.completions.requests.ResultEntry; +import com.zhongan.devpilot.enums.EditorActionEnum; +import com.zhongan.devpilot.integrations.llms.LlmProviderFactory; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotInstructCompletionRequest; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; +import com.zhongan.devpilot.statusBar.DevPilotStatusBarBaseWidget; +import com.zhongan.devpilot.statusBar.status.DevPilotStatusEnum; +import com.zhongan.devpilot.util.LoginUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static com.zhongan.devpilot.completions.general.StaticConfig.MAX_COMPLETIONS; +import static com.zhongan.devpilot.completions.general.StaticConfig.MAX_INSTRUCT_COMPLETION_TOKENS; +import static com.zhongan.devpilot.completions.general.StaticConfig.MIN_CHAT_COMPLETION_MESSAGE_LENGTH; +import static com.zhongan.devpilot.completions.general.StaticConfig.PREFIX_MAX_OFFSET; +import static com.zhongan.devpilot.completions.general.StaticConfig.SUFFIX_MAX_OFFSET; + +public class CompletionFacade { + + public CompletionFacade() { + } + + @Nullable + public static String getFilename(@Nullable VirtualFile file) { + return ObjectUtils.doIfNotNull(file, VirtualFile::getPath); + } + + @Nullable + public AutocompleteResponse retrieveCompletions( + @NotNull Editor editor, + int offset, + @Nullable Integer tabSize, + @Nullable CompletionAdjustment completionAdjustment, + String completionType) { + try { + String filename = + getFilename(FileDocumentManager.getInstance().getFile(editor.getDocument())); + return retrieveCompletions(editor, offset, filename, tabSize, completionAdjustment, completionType); + } catch (Exception e) { + DevPilotStatusBarBaseWidget.update(editor.getProject(), LoginUtils.isLogin() ? DevPilotStatusEnum.LoggedIn : DevPilotStatusEnum.NotLoggedIn); + return null; + } + } + + @Nullable + private AutocompleteResponse retrieveCompletions( + @NotNull Editor editor, + int offset, + @Nullable String filename, + @Nullable Integer tabSize, + @Nullable CompletionAdjustment completionAdjustment, + String completionType) { + Document document = editor.getDocument(); + + int begin = Integer.max(0, offset - PREFIX_MAX_OFFSET); + int end = Integer.min(document.getTextLength(), offset + SUFFIX_MAX_OFFSET); + AutocompleteRequest req = new AutocompleteRequest(); + req.before = document.getText(new TextRange(begin, offset)); + req.after = document.getText(new TextRange(offset, end)); + req.filename = filename; + req.maxResults = MAX_COMPLETIONS; + req.regionIncludesBeginning = (begin == 0); + req.regionIncludesEnd = (end == document.getTextLength()); + req.offset = offset; + req.line = document.getLineNumber(offset); + req.character = offset - document.getLineStartOffset(req.line); + req.indentationSize = tabSize; + req.sdkPath = getSdkPath(editor); + + if (completionAdjustment != null) { + completionAdjustment.adjustRequest(req); + } + + //Simulating request response. Calling OpenAI or returning a value. + DevPilotMessage devPilotMessage = new DevPilotMessage(); + devPilotMessage.setRole("user"); + String content = EditorActionEnum.CODE_COMPLETIONS.getPrompt() + .replace("{{offsetCode}}", document.getText(new TextRange(req.before.lastIndexOf("\n"), offset))) + .replace("{{selectedCode}}", getFileExtension(editor) + " " + req.before) + .replace("{{maxCompletionLength}}", MIN_CHAT_COMPLETION_MESSAGE_LENGTH); + devPilotMessage.setContent(content); + + devPilotMessage.setContent(req.before); + DevPilotInstructCompletionRequest request = new DevPilotInstructCompletionRequest(); + request.setPrompt(req.before); + request.setSuffix(req.after); + request.setMaxTokens(MAX_INSTRUCT_COMPLETION_TOKENS); + + DevPilotStatusBarBaseWidget.update(editor.getProject(), DevPilotStatusEnum.InCompletion); + request.setOffset(offset); + request.setEditor(editor); + if (!StringUtils.isEmpty(completionType)) { + request.setCompletionType(completionType); + } + final var response = new LlmProviderFactory().getLlmProvider(editor.getProject()).instructCompletion(request); + DevPilotStatusBarBaseWidget.update(editor.getProject(), LoginUtils.isLogin() ? DevPilotStatusEnum.LoggedIn : DevPilotStatusEnum.NotLoggedIn); + if (response == null) { + return null; + } + + AutocompleteResponse autocompleteResponse = new AutocompleteResponse(); + autocompleteResponse.oldPrefix = ""; + autocompleteResponse.userMessage = new String[] {}; + ResultEntry resultEntry = new ResultEntry(); + resultEntry.id = response.getId(); + resultEntry.newPrefix = response.getContent(); + resultEntry.oldSuffix = ""; + resultEntry.newSuffix = ""; + ResultEntry[] resultEntries = new ResultEntry[] {resultEntry}; + autocompleteResponse.results = resultEntries; + + if (completionAdjustment != null) { + completionAdjustment.adjustResponse(autocompleteResponse); + } + return autocompleteResponse; + } + + private String getSdkPath(Editor editor) { + if (editor == null) { + return null; + } + + VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(editor.getDocument()); + if (virtualFile == null + || virtualFile.getExtension() == null + || !virtualFile.getExtension().equals("java")) { + return null; + } + + if (editor.getProject() == null) { + return null; + } + + ProjectRootManager rootManager = ProjectRootManager.getInstance(editor.getProject()); + if (rootManager == null) { + return null; + } + + Sdk sdk = rootManager.getProjectSdk(); + if (sdk == null) { + return null; + } + + if (!sdk.getSdkType().isLocalSdk(sdk)) { + return null; + } + + String homePath = sdk.getHomePath(); + + if (!isValidJavaHome(homePath)) { + return null; + } + + return homePath; + } + + private boolean isValidJavaHome(String homePath) { + Path path = Paths.get(homePath, "bin", "java"); + return Files.exists(path); + } + +/* private int determineTimeoutBy(@NotNull String before) { + if (!suggestionsModeService.getSuggestionMode().isInlineEnabled()) { + return COMPLETION_TIME_THRESHOLD; + } + + int lastNewline = before.lastIndexOf("\n"); + String lastLine = lastNewline >= 0 ? before.substring(lastNewline) : ""; + boolean endsWithWhitespacesOnly = lastLine.trim().isEmpty(); + return endsWithWhitespacesOnly ? NEWLINE_COMPLETION_TIME_THRESHOLD : COMPLETION_TIME_THRESHOLD; + }*/ + + private String getFileExtension(Editor editor) { + if (editor == null) { + return null; + } + + VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(editor.getDocument()); + if (virtualFile == null || virtualFile.getExtension() == null) { + return null; + } + + return virtualFile.getExtension(); + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/prediction/DevPilotCompletion.java b/src/main/java/com/zhongan/devpilot/completions/prediction/DevPilotCompletion.java new file mode 100644 index 00000000..5563309a --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/prediction/DevPilotCompletion.java @@ -0,0 +1,169 @@ +package com.zhongan.devpilot.completions.prediction; + +import com.intellij.codeInsight.lookup.impl.LookupCellRenderer; +import com.intellij.openapi.util.TextRange; +import com.intellij.util.containers.FList; +import com.zhongan.devpilot.completions.Completion; +import com.zhongan.devpilot.completions.general.CompletionKind; +import com.zhongan.devpilot.completions.general.SuggestionTrigger; +import com.zhongan.devpilot.completions.requests.CompletionMetadata; + +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +public class DevPilotCompletion implements Completion { + public final String id; + + public final String oldPrefix; + + public final String newPrefix; + + public final String oldSuffix; + + public final String newSuffix; + + public final int index; + + public String cursorPrefix; + + public String cursorSuffix; + + public SuggestionTrigger suggestionTrigger; + + @Nullable // if new plugin with old binary + public CompletionMetadata completionMetadata; + + private String fullSuffix = null; + + public DevPilotCompletion( + String id, + String oldPrefix, + String newPrefix, + String oldSuffix, + String newSuffix, + int index, + String cursorPrefix, + String cursorSuffix, + @Nullable CompletionMetadata completionMetadata, + SuggestionTrigger suggestionTrigger) { + this.id = id; + this.oldPrefix = oldPrefix; + this.newPrefix = newPrefix; + this.oldSuffix = oldSuffix; + this.newSuffix = newSuffix; + this.index = index; + this.cursorPrefix = cursorPrefix; + this.cursorSuffix = cursorSuffix; + this.completionMetadata = completionMetadata; + this.suggestionTrigger = suggestionTrigger; + } + + public DevPilotCompletion createAdjustedCompletion(String oldPrefix, String cursorPrefix) { + return new DevPilotCompletion( + this.id, + oldPrefix, + this.newPrefix, + this.oldSuffix, + this.newSuffix, + this.index, + cursorPrefix, + this.cursorSuffix, + this.completionMetadata, + this.suggestionTrigger); + } + + public String getSuffix() { + if (fullSuffix != null) { + return fullSuffix; + } + + String itemText = this.newPrefix + this.newSuffix; + String prefix = this.oldPrefix; + if (prefix.isEmpty()) { + return fullSuffix = itemText; + } + + FList fragments = LookupCellRenderer.getMatchingFragments(prefix, itemText); + if (fragments != null && !fragments.isEmpty()) { + List list = new ArrayList<>(fragments); + return fullSuffix = itemText.substring(list.get(list.size() - 1).getEndOffset()); + } + + return fullSuffix = ""; + } + + public int getNetLength() { + return getSuffix().length(); + } + + @Override + public boolean isSnippet() { + if (this.completionMetadata == null || this.completionMetadata.getCompletionKind() == null) { + return false; + } + + return this.completionMetadata.getCompletionKind() == CompletionKind.Snippet; + } + + public String getOldPrefix() { + return oldPrefix; + } + + public String getNewPrefix() { + return newPrefix; + } + + public String getOldSuffix() { + return oldSuffix; + } + + public String getNewSuffix() { + return newSuffix; + } + + public int getIndex() { + return index; + } + + public String getCursorPrefix() { + return cursorPrefix; + } + + public void setCursorPrefix(String cursorPrefix) { + this.cursorPrefix = cursorPrefix; + } + + public String getCursorSuffix() { + return cursorSuffix; + } + + public void setCursorSuffix(String cursorSuffix) { + this.cursorSuffix = cursorSuffix; + } + + public SuggestionTrigger getSuggestionTrigger() { + return suggestionTrigger; + } + + public void setSuggestionTrigger(SuggestionTrigger suggestionTrigger) { + this.suggestionTrigger = suggestionTrigger; + } + + public CompletionMetadata getCompletionMetadata() { + return completionMetadata; + } + + public void setCompletionMetadata(CompletionMetadata completionMetadata) { + this.completionMetadata = completionMetadata; + } + + public String getFullSuffix() { + return fullSuffix; + } + + public void setFullSuffix(String fullSuffix) { + this.fullSuffix = fullSuffix; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/requests/AutocompleteRequest.java b/src/main/java/com/zhongan/devpilot/completions/requests/AutocompleteRequest.java new file mode 100644 index 00000000..4020ca87 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/requests/AutocompleteRequest.java @@ -0,0 +1,133 @@ +package com.zhongan.devpilot.completions.requests; + +import com.google.gson.annotations.SerializedName; + +import org.jetbrains.annotations.Nullable; + +public class AutocompleteRequest { + public String before; + + public String after; + + public String filename; + + @SerializedName(value = "region_includes_beginning") + public boolean regionIncludesBeginning; + + @SerializedName(value = "region_includes_end") + public boolean regionIncludesEnd; + + @SerializedName(value = "max_num_results") + public int maxResults; + + public int offset; + + public int line; + + public int character; + + @Nullable + public Integer indentationSize; + + @Nullable + public Boolean cachedOnly; + + @SerializedName(value = "sdk_path") + public String sdkPath; + + public String getBefore() { + return before; + } + + public void setBefore(String before) { + this.before = before; + } + + public String getAfter() { + return after; + } + + public void setAfter(String after) { + this.after = after; + } + + public String getFilename() { + return filename; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public boolean isRegionIncludesBeginning() { + return regionIncludesBeginning; + } + + public void setRegionIncludesBeginning(boolean regionIncludesBeginning) { + this.regionIncludesBeginning = regionIncludesBeginning; + } + + public boolean isRegionIncludesEnd() { + return regionIncludesEnd; + } + + public void setRegionIncludesEnd(boolean regionIncludesEnd) { + this.regionIncludesEnd = regionIncludesEnd; + } + + public int getMaxResults() { + return maxResults; + } + + public void setMaxResults(int maxResults) { + this.maxResults = maxResults; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getLine() { + return line; + } + + public void setLine(int line) { + this.line = line; + } + + public int getCharacter() { + return character; + } + + public void setCharacter(int character) { + this.character = character; + } + + public Integer getIndentationSize() { + return indentationSize; + } + + public void setIndentationSize(Integer indentationSize) { + this.indentationSize = indentationSize; + } + + public Boolean getCachedOnly() { + return cachedOnly; + } + + public void setCachedOnly(Boolean cachedOnly) { + this.cachedOnly = cachedOnly; + } + + public String getSdkPath() { + return sdkPath; + } + + public void setSdkPath(String sdkPath) { + this.sdkPath = sdkPath; + } +} diff --git a/src/main/java/com/zhongan/devpilot/completions/requests/AutocompleteResponse.java b/src/main/java/com/zhongan/devpilot/completions/requests/AutocompleteResponse.java new file mode 100644 index 00000000..af150f00 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/requests/AutocompleteResponse.java @@ -0,0 +1,11 @@ +package com.zhongan.devpilot.completions.requests; + +public class AutocompleteResponse { + public String oldPrefix; + + public ResultEntry[] results; + + public String[] userMessage; + + public boolean isLocked; +} diff --git a/src/main/java/com/zhongan/devpilot/completions/requests/CompletionMetadata.java b/src/main/java/com/zhongan/devpilot/completions/requests/CompletionMetadata.java new file mode 100644 index 00000000..085d578c --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/requests/CompletionMetadata.java @@ -0,0 +1,45 @@ +package com.zhongan.devpilot.completions.requests; + +import com.zhongan.devpilot.completions.general.CompletionKind; + +import java.util.Map; + +public class CompletionMetadata { + private String detail; + + private CompletionKind completionKind; + + private Map snippetContext; + + private Boolean isCached; + + private Boolean deprecated; + + public CompletionMetadata(String detail, CompletionKind completionKind, Map snippetContext, Boolean isCached, Boolean deprecated) { + this.detail = detail; + this.completionKind = completionKind; + this.snippetContext = snippetContext; + this.isCached = isCached; + this.deprecated = deprecated; + } + + public String getDetail() { + return detail; + } + + public CompletionKind getCompletionKind() { + return completionKind; + } + + public Map getSnippetContext() { + return snippetContext; + } + + public Boolean getIsCached() { + return isCached; + } + + public Boolean getIsDeprecated() { + return deprecated != null ? deprecated : false; + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/completions/requests/ResultEntry.java b/src/main/java/com/zhongan/devpilot/completions/requests/ResultEntry.java new file mode 100644 index 00000000..cb1c8570 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/completions/requests/ResultEntry.java @@ -0,0 +1,25 @@ +package com.zhongan.devpilot.completions.requests; + +import com.zhongan.devpilot.completions.Completion; +import com.zhongan.devpilot.completions.general.CompletionKind; + +public class ResultEntry implements Completion { + public String id; + + public String newPrefix; + + public String oldSuffix; + + public String newSuffix; + + public CompletionMetadata completionMetadata; + + @Override + public boolean isSnippet() { + if (this.completionMetadata == null) { + return false; + } + + return this.completionMetadata.getCompletionKind() == CompletionKind.Snippet; + } +} diff --git a/src/main/java/com/zhongan/devpilot/constant/DefaultConst.java b/src/main/java/com/zhongan/devpilot/constant/DefaultConst.java index d4be0a8d..282d07d9 100644 --- a/src/main/java/com/zhongan/devpilot/constant/DefaultConst.java +++ b/src/main/java/com/zhongan/devpilot/constant/DefaultConst.java @@ -1,5 +1,6 @@ package com.zhongan.devpilot.constant; +import com.zhongan.devpilot.util.ConfigBundleUtils; import com.zhongan.devpilot.util.DevPilotMessageBundle; public class DefaultConst { @@ -7,14 +8,37 @@ public class DefaultConst { private DefaultConst() { } - public static final String MAX_TOKEN_EXCEPTION_MSG = DevPilotMessageBundle.get("devpilot.chatWindow.context.overflow"); + public static final String GPT_35_MAX_TOKEN_EXCEPTION_MSG = DevPilotMessageBundle.get("devpilot.chatWindow.context.overflow"); - public static final int TOKEN_MAX_LENGTH = 4096; + public static final int GPT_35_TOKEN_MAX_LENGTH = 16384; - public static final int ENGLISH_CONTENT_MAX_LENGTH = 12288; + public static final int CONVERSATION_WINDOW_LENGTH = 8; - public static final int CHINESE_CONTENT_MAX_LENGTH = 1638; + public static final String DEFAULT_SOURCE_STRING = "JetBrains IDE"; - public static final String DEFAULT_CODE_LANGUAGE = "java"; + public static final String AI_GATEWAY_INSTRUCT_COMPLETION = "/devpilot/v1/code-completion/default"; + public static final String TELEMETRY_HOST = ConfigBundleUtils.getConfig("devpilot.telemetry.host", "http://localhost:8085"); + + public static final String TELEMETRY_LIKE_PATH = "/devpilot/v1/conversation-messages/%s"; + + public static final String TELEMETRY_CHAT_ACCEPT_PATH = "/devpilot/v1/conversation-messages/%s/accepted"; + + public static final String TELEMETRY_COMPLETION_ACCEPT_PATH = "/devpilot/v1/completion-messages/%s"; + + public static final String LOGIN_AUTH_URL = ConfigBundleUtils.getConfig("devpilot.login-h5.host", "http://localhost:8085") + "/login?backUrl=%s&source=%s"; + + public static final String LOGIN_CALLBACK_URL = "http://127.0.0.1:%s/login/auth/callback"; + + public static final String TRIAL_DEFAULT_HOST = ConfigBundleUtils.getConfig("devpilot.trial.host", "http://localhost:8085"); + + public static final String TRIAL_DEFAULT_MODEL = "azure/gpt-3.5-turbo"; + + public static final String AI_GATEWAY_DEFAULT_HOST = ConfigBundleUtils.getConfig("devpilot.ai-gateway.host", "http://localhost:8085"); + + public static final String RAG_DEFAULT_HOST = ConfigBundleUtils.getConfig("devpilot.rag.host", "http://localhost:8085") + "/devpilot/v1/rag/git_repo/embedding_info/"; + + public static final boolean AUTH_ON = true; + + public static final boolean TELEMETRY_ON = true; } \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/constant/PromptConst.java b/src/main/java/com/zhongan/devpilot/constant/PromptConst.java index 916a3695..6ba832c9 100644 --- a/src/main/java/com/zhongan/devpilot/constant/PromptConst.java +++ b/src/main/java/com/zhongan/devpilot/constant/PromptConst.java @@ -10,7 +10,7 @@ private PromptConst() { "- quote variable name with single backtick such as `name`.\n" + "- quote code block with triple backticks such as ```...```"; - public static final String ANSWER_IN_CHINESE = "\nPlease answer in Chinese."; + public static final String ANSWER_IN_CHINESE = "\n\n请用中文回答"; public static final String GENERATE_COMMIT = "Summarize the git diff with a concise and descriptive commit message. Adopt the imperative mood, present tense, active voice, and include relevant verbs. Remember that your entire response will be directly used as the git commit message."; diff --git a/src/main/java/com/zhongan/devpilot/enums/ChatActionTypeEnum.java b/src/main/java/com/zhongan/devpilot/enums/ChatActionTypeEnum.java new file mode 100644 index 00000000..3ad3218c --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/enums/ChatActionTypeEnum.java @@ -0,0 +1,21 @@ +package com.zhongan.devpilot.enums; + +/** + * Used for telemetry upload + */ +public enum ChatActionTypeEnum { + INSERT("INSERT"), + REPLACE("REPLACE"), + NEW_FILE("NEW_FILE"), + COPY("COPY"); + + private final String type; + + ChatActionTypeEnum(String type) { + this.type = type; + } + + public String getType() { + return type; + } +} diff --git a/src/main/java/com/zhongan/devpilot/enums/EditorActionEnum.java b/src/main/java/com/zhongan/devpilot/enums/EditorActionEnum.java index 7f515048..47858ea5 100644 --- a/src/main/java/com/zhongan/devpilot/enums/EditorActionEnum.java +++ b/src/main/java/com/zhongan/devpilot/enums/EditorActionEnum.java @@ -4,11 +4,50 @@ public enum EditorActionEnum { PERFORMANCE_CHECK("devpilot.action.performance.check", "Performance check in the following code", - "{{selectedCode}}\nGiving the code above, please fix any performance issues." + - "\nRemember you are very familiar with performance optimization.\n"), + "Perform a performance check on the specified code. Only identify and report on the aspects where actual performance issues are found. Do not list out aspects that do not have issues.\n" + + "The check may focus on, but is not limited to, the following aspects:\n" + + "1.Algorithmic Efficiency: Check for inefficient algorithms that may slow down the program.\n" + + "2.Data Structures: Evaluate the use of data structures for potential inefficiencies.\n" + + "3.Loops: Inspect loops for unnecessary computations or operations that could be moved outside.\n" + + "4.Method Calls: Look for frequent method calls.\n" + + "5.Object Creation: Check for unnecessary object creation.\n" + + "6.Use of Libraries: Review the use of library methods for potential inefficiencies.\n" + + "7.Concurrency: If multithreading is used, ensure efficient operation and absence of bottlenecks or deadlocks.\n" + + "8.I/O Operations: Look for inefficient use of I/O operations.\n" + + "9.Database Queries: If the code interacts with a database, check for inefficient or excessive queries.\n" + + "10.Network Calls: If the code makes network calls, consider their efficiency and potential impact on performance.\n" + + "Remember, the goal of a performance check is to identify potential performance issues in the code, not to optimize every single detail. Always measure before and after making changes to see if the changes had the desired effect.\n" + + "\n" + + "The following code is being analyzed for performance: \n" + + "{{selectedCode}} \n" + + "For each identified issue, provide a report in the following format:\n" + + "\n" + + "### performance review report\n" + + "1. **the specific problem summary**\n" + + " - Code: [Provide the problematic code snippet or a range of lines in the code where the issue was found.]\n" + + " - Problem: [Describe the specific problem]\n" + + " - Suggestion: [Provide a suggestion for fixing the issue]" + ), GENERATE_COMMENTS("devpilot.action.generate.comments", "Generate comments in the following code", - "{{selectedCode}}\nGiving the code above, please generate code comments, return code with comments."), + "Write inline comments for the key parts of the specified function.\n" + + "The comments should explain what each part of the function does in a clear and concise manner.\n" + + "Avoid commenting on every single line of code, as this can make the code harder to read and maintain. Instead, focus on the parts of the function that are complex, important, or not immediately obvious.\n" + + "Remember, the goal of inline comments is to help other developers understand the code, not to explain every single detail.\n\n" + + "The comment is being written for the following code: \n" + + "{{selectedCode}} "), + + GENERATE_METHOD_COMMENTS("devpilot.action.generate.method.comments", "", + "Write a function comment for the specified function in the appropriate style for the programming language being used.\n" + + "The comment should start with a brief summary of what the function does. This should be followed by a detailed description, if necessary.\n" + + "Then, document each parameter, explaining the purpose of each one.\n" + + "If the function returns a value, describe what the function returns.\n" + + "If the function throws exceptions, document each exception and under what conditions it is thrown.\n" + + "Make sure the comment is clear, concise, and free of spelling and grammar errors.\n" + + "The comment should help other developers understand what the function does, how it works, and how to use it.\n" + + "Please note that the function definition is not included in this task, only the function comment is required.\n\n" + + "The comment is being written for the following code: \n" + + "{{selectedCode}} "), GENERATE_TESTS("devpilot.action.generate.tests", "Generate Tests in the following code", "{{selectedCode}}\nGiving the {{language:unknown}} code above, " + @@ -19,22 +58,57 @@ public enum EditorActionEnum { "please state it and give suggestions instead."), FIX_THIS("devpilot.action.fix", "Fix This in the following code", - "{{selectedCode}}\nGiving the code above, please help to fix it:\n\n" + - "- Fix any typos or grammar issues.\n" + - "- Use better names as replacement to magic numbers or arbitrary acronyms.\n" + - "- Simplify the code so that it's more strait forward and easy to understand.\n" + - "- Optimize it for performance reasons.\n" + - "- Refactor it using best practice in software engineering.\n" + "\nMust only provide the code to be fixed and explain why it should be fixed.\n"), + "Perform a code fix on the specified code. Only identify and make changes in the aspects where actual issues are found. Do not list out aspects that do not have issues. \n" + + "The fix may focus on, but is not limited to, the following aspects:\n" + + "1. Bug Fixes: Identify and correct any errors or bugs in the code. Ensure the fix doesn't introduce new bugs.\n" + + "2. Performance Improvements: Look for opportunities to optimize the code for better performance. This could involve changing algorithms, reducing memory usage, or other optimizations.\n" + + "3. Code Clarity: Make the code easier to read and understand. This could involve renaming variables for clarity, breaking up complex functions into smaller ones.\n" + + "4. Code Structure: Improve the organization of the code. This could involve refactoring the code to improve its structure, or rearranging code for better readability.\n" + + "5. Coding Standards: Ensure the code follows the agreed-upon coding standards. This includes naming conventions, comment style, indentation, and other formatting rules.\n" + + "6. Error Handling: Improve error handling in the code. The code should fail gracefully and not expose any sensitive information when an error occurs.\n" + + "Remember, the goal of a code fix is to improve the quality of the code and make it work correctly, efficiently, and in line with the requirements. Always test the code after making changes to ensure it still works as expected.\n" + + "\n" + + "The following code is being fixed: \n" + + "{{selectedCode}} \n" + + "For each identified issue, provide an explanation of the problem and describe how it will be fixed. Then, present the fixed code."), REVIEW_CODE("devpilot.action.review", "Review code in the following code", - "{{selectedCode}}\nGiving the code above, please review the code line by line:\n\n" + - "- Think carefully, you should be extremely careful.\n" + - "- Find out if any bugs exists.\n" + - "- Reveal any bad smell in the code.\n" + - "- Give optimization or best practice suggestion.\n"), + "Perform a code review on the specified code. Only identify and report on the aspects where actual issues are found. Do not list out aspects that do not have issues.\n" + + "The review may focus on, but is not limited to, the following aspects:\n" + + "1. Code Clarity: Is the code easy to read and understand?\n" + + "2. Code Structure: Is the code well-structured and organized?\n" + + "3. Coding Standards: Does the code follow the agreed-upon coding standards?\n" + + "4. Error Handling: Does the code handle errors gracefully?\n" + + "5. Logic Errors: Are there any obvious mistakes in the code?\n" + + "6. Security: Are there any potential security vulnerabilities in the code?\n" + + "Remember, the goal of a code review is to improve the quality of the code and catch bugs before the code is executed. Always provide constructive feedback and explain why a change might be necessary.\n" + + "\n" + + "The following code is being reviewed: \n" + + "{{selectedCode}} \n" + + "For each identified issue, provide a report in the following format:\n" + + "\n" + + "### code review report" + + "1. **the specific problem summary**\n" + + " - Code: [Provide the problematic code snippet or a range of lines in the code where the issue was found.]\n" + + " - Problem: [Describe the specific problem]\n" + + " - Suggestion: [Provide a suggestion for fixing the issue]"), EXPLAIN_THIS("devpilot.action.explain", "Explain this in the following code", - "{{selectedCode}}\nGiving the code above, please explain it in detail, line by line.\n"); + "Write a detailed explanation for the specified code.\n" + + "Begin with a brief summary outlining its purpose and functionality.\n" + + "Then, dissect the code line by line or block by block.After each segment of code, provide a detailed explanation of its role and operation.\n" + + "Aim for clarity and conciseness in your explanation.Ensure that the explanation is comprehensive, covering all aspects of the code's operation, and free of spelling and grammar errors.\n" + + "The explanation should be interwoven with the code, providing a clear understanding of each part as it appears in the code sequence.\n" + + "Avoid including a separate copy of the complete original code, as the code is already explained in segments.\n\n" + + "The explanation is being written for the following code: \n" + + "{{selectedCode}} "), + + CODE_COMPLETIONS("devpilot.action.completions", "code completions", + "You are a code completion service, please try to auto complete the code below at {{offsetCode}}, " + + "only output the code, don't try to have conversation with user.```{{selectedCode}}```" +// + +// "\nThe completion code returned is controlled within {{maxCompletionLength}} bytes within" + ); private final String label; diff --git a/src/main/java/com/zhongan/devpilot/enums/LoginTypeEnum.java b/src/main/java/com/zhongan/devpilot/enums/LoginTypeEnum.java new file mode 100644 index 00000000..9932d0c3 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/enums/LoginTypeEnum.java @@ -0,0 +1,34 @@ +package com.zhongan.devpilot.enums; + +public enum LoginTypeEnum { + ZA("ZA", "众安保险SSO"), + ZA_TI("ZA_TI", "众安国际SSO"), + WX("WX", "微信公众号"); + + // login type + private final String type; + + private final String displayName; + + LoginTypeEnum(String type, String displayName) { + this.type = type; + this.displayName = displayName; + } + + public static LoginTypeEnum getLoginTypeEnum(String type) { + for (LoginTypeEnum loginTypeEnum : LoginTypeEnum.values()) { + if (loginTypeEnum.getType().equals(type)) { + return loginTypeEnum; + } + } + return LoginTypeEnum.ZA; + } + + public String getType() { + return type; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/com/zhongan/devpilot/enums/ModelServiceEnum.java b/src/main/java/com/zhongan/devpilot/enums/ModelServiceEnum.java index 6a617015..cb1ffdd1 100644 --- a/src/main/java/com/zhongan/devpilot/enums/ModelServiceEnum.java +++ b/src/main/java/com/zhongan/devpilot/enums/ModelServiceEnum.java @@ -4,7 +4,7 @@ public enum ModelServiceEnum { OPENAI("OpenAI", "OpenAI Service"), LLAMA("LLaMA", "Code LLaMA (Locally)"), AIGATEWAY("AIGateway", "AI Gateway"), - OLLAMA("Ollama", "Ollama Service"); + TRIAL("Trial", "Trial Service (Free)"); // model name private final String name; diff --git a/src/main/java/com/zhongan/devpilot/enums/OpenAIModelNameEnum.java b/src/main/java/com/zhongan/devpilot/enums/OpenAIModelNameEnum.java new file mode 100644 index 00000000..3f303dd7 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/enums/OpenAIModelNameEnum.java @@ -0,0 +1,51 @@ +package com.zhongan.devpilot.enums; + +public enum OpenAIModelNameEnum { + GPT3_5_TURBO("gpt-3.5-turbo", "gpt-3.5-turbo"), + GPT3_5_TURBO_16K("gpt-3.5-turbo-16k", "gpt-3.5-turbo(16k)"), + GPT4("gpt-4", "gpt-4"), + GPT4_32K("gpt-4-32k", "gpt-4(32k)"), + CUSTOM("custom", "Custom Model"); + + private String name; + + private String displayName; + + OpenAIModelNameEnum(String name, String displayName) { + this.name = name; + this.displayName = displayName; + } + + public static OpenAIModelNameEnum fromName(String name) { + if (name == null) { + return GPT3_5_TURBO; + } + for (OpenAIModelNameEnum type : OpenAIModelNameEnum.values()) { + if (type.getName().equals(name)) { + return type; + } + } + return GPT3_5_TURBO; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/com/zhongan/devpilot/enums/ZaSsoEnum.java b/src/main/java/com/zhongan/devpilot/enums/ZaSsoEnum.java new file mode 100644 index 00000000..90f3ab58 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/enums/ZaSsoEnum.java @@ -0,0 +1,42 @@ +package com.zhongan.devpilot.enums; + +public enum ZaSsoEnum { + ZA("ZA", "众安保险SSO"), + ZA_TI("ZA_TI", "众安国际SSO"); + + // sso name + private final String name; + + // sso display name + private final String displayName; + + ZaSsoEnum(String name, String displayName) { + this.name = name; + this.displayName = displayName; + } + + public String getName() { + return name; + } + + public String getDisplayName() { + return displayName; + } + + public static ZaSsoEnum fromName(String name) { + if (name == null) { + return ZA; + } + for (ZaSsoEnum type : ZaSsoEnum.values()) { + if (type.getName().equals(name)) { + return type; + } + } + return ZA; + } + + @Override + public String toString() { + return displayName; + } +} diff --git a/src/main/java/com/zhongan/devpilot/exception/DevPilotErrorReporter.java b/src/main/java/com/zhongan/devpilot/exception/DevPilotErrorReporter.java new file mode 100644 index 00000000..467530bf --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/exception/DevPilotErrorReporter.java @@ -0,0 +1,69 @@ +package com.zhongan.devpilot.exception; + +import com.intellij.diagnostic.ITNReporter; +import com.intellij.diagnostic.PluginException; +import com.intellij.ide.plugins.PluginUtil; +import com.intellij.openapi.diagnostic.IdeaLoggingEvent; +import com.intellij.openapi.diagnostic.SubmittedReportInfo; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.util.Consumer; +import com.zhongan.devpilot.util.DevPilotMessageBundle; + +import java.awt.Component; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * see {@link com.intellij.diagnostic.DefaultIdeaErrorLogger} + */ +public class DevPilotErrorReporter extends ITNReporter { + + private static final String DEPRECATED_DEFAULT_PREFIX = "The default implementation of method"; + + private static final String DEPRECATED_DEFAULT_SUFFIX = "is deprecated, you need to override it in"; + + private static final String DEPRECATED_USAGE = "is deprecated and going to be removed soon."; + + private final PluginId devpilotPluginId = PluginId.getId("com.zhongan.devPilot"); + + + /** + * Ignore deprecated method error, in internal model, still receiver warning + * @param event + * @return + */ + @Override + public boolean showErrorInRelease(IdeaLoggingEvent event) { + boolean isDevpilotDeprecatedUseNotice = false; + Throwable t = event.getThrowable(); + PluginId pluginId = PluginUtil.getInstance().findPluginId(t); + if (Objects.equals(pluginId, devpilotPluginId)) { + if (t instanceof PluginException) { + PluginException pluginException = (PluginException) t; + String message = pluginException.getMessage(); + if (StringUtils.isNoneBlank(message)) { + if (message.contains(DEPRECATED_USAGE) || (message.contains(DEPRECATED_DEFAULT_PREFIX) && message.contains(DEPRECATED_DEFAULT_SUFFIX))) { + isDevpilotDeprecatedUseNotice = true; + } + } + } + } + return !isDevpilotDeprecatedUseNotice; + } + + @NotNull + @Override + public String getReportActionText() { + return DevPilotMessageBundle.get("devpilot.error.report"); + } + + @Override + public boolean submit(IdeaLoggingEvent @NotNull [] events, @Nullable String additionalInfo, @NotNull Component parentComponent, @NotNull Consumer consumer) { + // do nothing + return true; + } + +} diff --git a/src/main/java/com/zhongan/devpilot/gui/toolwindows/chat/DevPilotChatToolWindow.java b/src/main/java/com/zhongan/devpilot/gui/toolwindows/chat/DevPilotChatToolWindow.java index e1b59b9e..7b835b76 100644 --- a/src/main/java/com/zhongan/devpilot/gui/toolwindows/chat/DevPilotChatToolWindow.java +++ b/src/main/java/com/zhongan/devpilot/gui/toolwindows/chat/DevPilotChatToolWindow.java @@ -5,23 +5,27 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.SystemInfo; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleManager; import com.intellij.ui.jcef.JBCefBrowser; import com.intellij.ui.jcef.JBCefBrowserBase; import com.intellij.ui.jcef.JBCefJSQuery; +import com.zhongan.devpilot.enums.ChatActionTypeEnum; import com.zhongan.devpilot.enums.EditorActionEnum; import com.zhongan.devpilot.enums.SessionTypeEnum; import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; import com.zhongan.devpilot.util.ConfigChangeUtils; import com.zhongan.devpilot.util.EditorUtils; +import com.zhongan.devpilot.util.JetbrainsVersionUtils; import com.zhongan.devpilot.util.JsonUtils; +import com.zhongan.devpilot.util.LoginUtils; import com.zhongan.devpilot.util.NewFileUtils; +import com.zhongan.devpilot.util.TelemetryUtils; import com.zhongan.devpilot.webview.DevPilotCustomHandlerFactory; import com.zhongan.devpilot.webview.model.CodeActionModel; import com.zhongan.devpilot.webview.model.CodeReferenceModel; -import com.zhongan.devpilot.webview.model.CopyModel; import com.zhongan.devpilot.webview.model.JsCallModel; import com.zhongan.devpilot.webview.model.MessageModel; @@ -56,8 +60,20 @@ public synchronized JBCefBrowser jbCefBrowser() { private void load() { JBCefBrowser browser; try { - browser = JBCefBrowser.createBuilder().setOffScreenRendering(false).build(); - } catch (Exception e) { + boolean isOffScreenRendering = true; + if (SystemInfo.isMac) { + isOffScreenRendering = false; + } else if (!SystemInfo.isLinux && !SystemInfo.isUnix) { + if (SystemInfo.isWindows) { + isOffScreenRendering = true; + } + } else { + isOffScreenRendering = JetbrainsVersionUtils.isVersionLaterThan233(); + } + + browser = JBCefBrowser.createBuilder().setOffScreenRendering(isOffScreenRendering).createBrowser(); + + } catch (Throwable e) { browser = new JBCefBrowser(); } @@ -112,6 +128,9 @@ private void registerJsCallJavaHandler(JBCefBrowser browser) { } insertAtCaret(codeActionModel.getContent()); + + TelemetryUtils.chatAccept(codeActionModel, ChatActionTypeEnum.INSERT); + return new JBCefJSQuery.Response("success"); } case "ReplaceSelectedCode": { @@ -123,6 +142,9 @@ private void registerJsCallJavaHandler(JBCefBrowser browser) { } replaceSelectionCode(codeActionModel.getContent()); + + TelemetryUtils.chatAccept(codeActionModel, ChatActionTypeEnum.REPLACE); + return new JBCefJSQuery.Response("success"); } case "CreateNewFile": { @@ -141,7 +163,9 @@ private void registerJsCallJavaHandler(JBCefBrowser browser) { ApplicationManager.getApplication().invokeLater( () -> NewFileUtils.createNewFile(project, codeActionModel.getContent(), - userMessage.getCodeRef())); + userMessage.getCodeRef(), codeActionModel.getLang())); + + TelemetryUtils.chatAccept(codeActionModel, ChatActionTypeEnum.NEW_FILE); return new JBCefJSQuery.Response("success"); } @@ -186,14 +210,51 @@ private void registerJsCallJavaHandler(JBCefBrowser browser) { } case "CopyCode": { var payload = jsCallModel.getPayload(); - var copyModel = JsonUtils.fromJson(JsonUtils.toJson(payload), CopyModel.class); + var codeActionModel = JsonUtils.fromJson(JsonUtils.toJson(payload), CodeActionModel.class); - if (copyModel == null || copyModel.getContent() == null) { + if (codeActionModel == null || codeActionModel.getContent() == null) { return new JBCefJSQuery.Response("error"); } var clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); - clipboard.setContents(new StringSelection(copyModel.getContent()), null); + clipboard.setContents(new StringSelection(codeActionModel.getContent()), null); + + TelemetryUtils.chatAccept(codeActionModel, ChatActionTypeEnum.COPY); + + return new JBCefJSQuery.Response("success"); + } + case "OpenFile": { + var payload = jsCallModel.getPayload(); + var codeActionModel = JsonUtils.fromJson(JsonUtils.toJson(payload), CodeActionModel.class); + + if (codeActionModel == null || codeActionModel.getContent() == null) { + return new JBCefJSQuery.Response("error"); + } + + String relativePath = codeActionModel.getContent(); + String repo = codeActionModel.getRepo(); + ApplicationManager.getApplication().invokeLater( + () -> EditorUtils.openFileByRelativePath(repo, project, relativePath)); + + return new JBCefJSQuery.Response("success"); + } + case "Login": { + LoginUtils.gotoLogin(); + return new JBCefJSQuery.Response("success"); + } + case "DislikeMessage": + case "LikeMessage": { + var payload = jsCallModel.getPayload(); + var messageModel = JsonUtils.fromJson(JsonUtils.toJson(payload), MessageModel.class); + if (messageModel == null || messageModel.getId() == null) { + return new JBCefJSQuery.Response("error"); + } + + var id = messageModel.getId(); + var action = !command.equals("DislikeMessage"); + + TelemetryUtils.messageFeedback(id, action); + return new JBCefJSQuery.Response("success"); } default: return new JBCefJSQuery.Response("success"); @@ -214,9 +275,11 @@ public void onLoadStart(CefBrowser browser, CefFrame frame, CefRequest.Transitio 0 ); - var format = "window.intellijConfig = {theme: '%s', locale: '%s', username: '%s'};"; + var format = "window.intellijConfig = {theme: '%s', locale: '%s', username: '%s', loggedIn: %s, env: '%s', version: '%s', platform: '%s'};"; var configModel = ConfigChangeUtils.configInit(); - var code = String.format(format, configModel.getTheme(), configModel.getLocale(), configModel.getUsername()); + var code = String.format(format, configModel.getTheme(), + configModel.getLocale(), configModel.getUsername(), configModel.isLoggedIn(), + configModel.getEnv(), configModel.getVersion(), configModel.getPlatform()); browser.executeJavaScript(code, null, 0); } diff --git a/src/main/java/com/zhongan/devpilot/gui/toolwindows/chat/DevPilotChatToolWindowService.java b/src/main/java/com/zhongan/devpilot/gui/toolwindows/chat/DevPilotChatToolWindowService.java index ccce7690..95b22792 100644 --- a/src/main/java/com/zhongan/devpilot/gui/toolwindows/chat/DevPilotChatToolWindowService.java +++ b/src/main/java/com/zhongan/devpilot/gui/toolwindows/chat/DevPilotChatToolWindowService.java @@ -6,7 +6,9 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.Balloon; import com.zhongan.devpilot.actions.editor.popupmenu.BasicEditorAction; +import com.zhongan.devpilot.constant.DefaultConst; import com.zhongan.devpilot.constant.PromptConst; import com.zhongan.devpilot.enums.EditorActionEnum; import com.zhongan.devpilot.enums.SessionTypeEnum; @@ -14,15 +16,20 @@ import com.zhongan.devpilot.integrations.llms.LlmProviderFactory; import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest; import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; +import com.zhongan.devpilot.util.BalloonAlertUtils; import com.zhongan.devpilot.util.DevPilotMessageBundle; import com.zhongan.devpilot.util.JsonUtils; import com.zhongan.devpilot.util.MessageUtil; +import com.zhongan.devpilot.util.TokenUtils; +import com.zhongan.devpilot.webview.model.EmbeddedModel; import com.zhongan.devpilot.webview.model.JavaCallModel; import com.zhongan.devpilot.webview.model.LocaleModel; +import com.zhongan.devpilot.webview.model.LoginModel; import com.zhongan.devpilot.webview.model.MessageModel; import com.zhongan.devpilot.webview.model.ThemeModel; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.Consumer; @@ -58,15 +65,20 @@ public String sendMessage(Integer sessionType, String message, Consumer var sessionTypeEnum = SessionTypeEnum.getEnumByCode(sessionType); if (SessionTypeEnum.INDEPENDENT.equals(sessionTypeEnum)) { // independent message can not update, just readonly - devPilotChatCompletionRequest.getMessages().add(userMessage); devPilotChatCompletionRequest.getMessages().add(MessageUtil.createSystemMessage(PromptConst.RESPONSE_FORMAT)); + devPilotChatCompletionRequest.getMessages().add(userMessage); } else { if (historyRequestMessageList.isEmpty()) { historyRequestMessageList.add(MessageUtil.createSystemMessage(PromptConst.RESPONSE_FORMAT)); } devPilotChatCompletionRequest.setStream(Boolean.TRUE); + if (message.startsWith("@repo")) { + clearRequestSession(); + historyRequestMessageList.add(MessageUtil.createSystemMessage(PromptConst.RESPONSE_FORMAT)); + } historyRequestMessageList.add(userMessage); - devPilotChatCompletionRequest.getMessages().addAll(historyRequestMessageList); + buildConversationWindowMemory(); + devPilotChatCompletionRequest.getMessages().addAll(copyHistoryRequestMessageList(historyRequestMessageList)); } callWebView(messageModel); @@ -93,7 +105,7 @@ public String sendMessage(Consumer callback) { historyRequestMessageList.add(MessageUtil.createSystemMessage(PromptConst.RESPONSE_FORMAT)); } devPilotChatCompletionRequest.setStream(Boolean.TRUE); - devPilotChatCompletionRequest.getMessages().addAll(historyRequestMessageList); + devPilotChatCompletionRequest.getMessages().addAll(copyHistoryRequestMessageList(historyRequestMessageList)); callWebView(MessageModel.buildLoadingMessage()); @@ -185,6 +197,7 @@ public void regenerateMessage() { var id = lastMessage.getId(); historyMessageList.removeIf(item -> item.getId().equals(id)); + historyRequestMessageList.removeIf(item -> item.getId().equals(id)); // todo handle real callback sendMessage(null); } @@ -196,6 +209,7 @@ public void handleActions(EditorActionEnum actionEnum) { ApplicationManager.getApplication().invokeLater(() -> { Editor editor = FileEditorManager.getInstance(project).getSelectedTextEditor(); if (editor == null || !editor.getSelectionModel().hasSelection()) { + BalloonAlertUtils.showWarningAlert(DevPilotMessageBundle.get("devpilot.alter.code.not.selected"), 0, -10, Balloon.Position.above); return; } myAction.fastAction(project, editor, editor.getSelectionModel().getSelectedText()); @@ -219,6 +233,32 @@ public MessageModel getUserMessage(String id) { return userMessage; } + private void buildConversationWindowMemory() { + // 先确保内容不超过 输入token限制 + List tokenCounts = TokenUtils.ComputeTokensFromMessagesUsingGPT35Enc(historyRequestMessageList); + int totalTokenCount = tokenCounts.get(0); + Collections.reverse(tokenCounts); + int keepCount = 1; + for (int i = 0; i < tokenCounts.size() - 1; i++) { + Integer tokenCount = tokenCounts.get(i); + if (totalTokenCount + tokenCount > DefaultConst.GPT_35_TOKEN_MAX_LENGTH) { + break; + } + totalTokenCount += tokenCount; + keepCount++; + } + int removeCount = historyRequestMessageList.size() - keepCount; + for (int i = 0; i < removeCount; i++) { + historyRequestMessageList.remove(1); + } + + // 再检查window size + removeCount = historyRequestMessageList.size() - DefaultConst.CONVERSATION_WINDOW_LENGTH; + for (int i = 0; i < removeCount; i++) { + historyRequestMessageList.remove(1); + } + } + private int getMessageIndex(String id) { var index = -1; for (int i = 0; i < historyMessageList.size(); i++) { @@ -231,6 +271,18 @@ private int getMessageIndex(String id) { return index; } + private List copyHistoryRequestMessageList(List historyRequestMessageList) { + List copiedList = new ArrayList<>(); + for (DevPilotMessage message : historyRequestMessageList) { + DevPilotMessage copiedMessage = new DevPilotMessage(); + copiedMessage.setContent(message.getContent()); + copiedMessage.setRole(message.getRole()); + copiedMessage.setId(message.getId()); + copiedList.add(copiedMessage); + } + return copiedList; + } + public void callWebView(JavaCallModel javaCallModel) { var browser = getDevPilotChatToolWindow().jbCefBrowser().getCefBrowser(); var json = JsonUtils.toJson(javaCallModel); @@ -244,7 +296,9 @@ public void callWebView(JavaCallModel javaCallModel) { } public void callErrorInfo(String content) { - callWebView(MessageModel.buildErrorMessage(content)); + var messageModel = MessageModel.buildInfoMessage(content); + callWebView(messageModel); + addMessage(messageModel); } public void callWebView(MessageModel messageModel) { @@ -271,12 +325,6 @@ public void callWebView() { } public void changeTheme(String theme) { - if (theme.contains("Light")) { - theme = "light"; - } else { - theme = "dark"; - } - var javaCallModel = new JavaCallModel(); javaCallModel.setCommand("ThemeChanged"); javaCallModel.setPayload(new ThemeModel(theme)); @@ -291,4 +339,20 @@ public void changeLocale(String locale) { callWebView(javaCallModel); } + + public void changeLoginStatus(boolean isLoggedIn) { + var javaCallModel = new JavaCallModel(); + javaCallModel.setCommand("ConfigurationChanged"); + javaCallModel.setPayload(new LoginModel(isLoggedIn)); + + callWebView(javaCallModel); + } + + public void presentRepoCodeEmbeddedState(boolean isEmbedded, String repoName) { + JavaCallModel javaCallModel = new JavaCallModel(); + javaCallModel.setCommand("PresentCodeEmbeddedState"); + javaCallModel.setPayload(new EmbeddedModel(isEmbedded, repoName)); + + callWebView(javaCallModel); + } } diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/LlmProvider.java b/src/main/java/com/zhongan/devpilot/integrations/llms/LlmProvider.java index 73e5e439..92a2b2c7 100644 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/LlmProvider.java +++ b/src/main/java/com/zhongan/devpilot/integrations/llms/LlmProvider.java @@ -4,14 +4,13 @@ import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest; import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionResponse; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotInstructCompletionRequest; import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; import com.zhongan.devpilot.integrations.llms.entity.DevPilotSuccessStreamingResponse; import com.zhongan.devpilot.util.JsonUtils; import com.zhongan.devpilot.util.OkhttpUtils; import com.zhongan.devpilot.webview.model.MessageModel; -import java.util.ArrayList; -import java.util.List; import java.util.UUID; import java.util.function.Consumer; @@ -30,12 +29,22 @@ public interface LlmProvider { DevPilotChatCompletionResponse chatCompletionSync(DevPilotChatCompletionRequest chatCompletionRequest); + DevPilotMessage instructCompletion(DevPilotInstructCompletionRequest instructCompletionRequest); + void interruptSend(); default void restoreMessage(MessageModel messageModel) { // default not restore message } + default void handleNoAuth(DevPilotChatToolWindowService service) { + var content = "Chat completion failed: Auth Failed"; + var assistantMessage = MessageModel.buildInfoMessage(content); + + service.callWebView(assistantMessage); + service.addMessage(assistantMessage); + } + default EventSource buildEventSource(Request request, DevPilotChatToolWindowService service, Consumer callback) { var time = System.currentTimeMillis(); @@ -45,7 +54,7 @@ default EventSource buildEventSource(Request request, return EventSources.createFactory(client).newEventSource(request, new EventSourceListener() { @Override public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) { - if ("[DONE]".equals(data)) { + if (data.equals("[DONE]")) { return; } @@ -55,16 +64,28 @@ public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Null interruptSend(); return; } + + boolean streaming = Boolean.TRUE; + + if (null != response.getRag()) { + var ragResp = response.getRag(); + var files = ragResp.getFiles(); + var app = ragResp.getApp(); + result.append("\n\n
"); + for (DevPilotSuccessStreamingResponse.RagFile file : files) { + result.append("
").append(file.getFile()).append("
"); + } + result.append("
\n\n"); + } else { + var choice = response.getChoices().get(0); + var finishReason = choice.getFinishReason(); - var choice = response.getChoices().get(0); - var finishReason = choice.getFinishReason(); - - if (choice.getDelta().getContent() != null) { - result.append(choice.getDelta().getContent()); + if (choice.getDelta().getContent() != null) { + result.append(choice.getDelta().getContent()); + } + streaming = !"stop".equals(finishReason); } - var streaming = !"stop".equals(finishReason); - var assistantMessage = MessageModel .buildAssistantMessage(response.getId(), time, result.toString(), streaming); @@ -89,7 +110,8 @@ public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @ var message = "Chat completion failed"; if (response != null && response.code() == 401) { - message = "Chat completion failed: Unauthorized"; + handleNoAuth(service); + return; } if (t != null) { @@ -108,8 +130,4 @@ public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable t, @ } }); } - - default List listModels(String host, String apiKey) { - return new ArrayList<>(); - } } diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/LlmProviderFactory.java b/src/main/java/com/zhongan/devpilot/integrations/llms/LlmProviderFactory.java index 85653b78..e73b2234 100644 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/LlmProviderFactory.java +++ b/src/main/java/com/zhongan/devpilot/integrations/llms/LlmProviderFactory.java @@ -2,11 +2,9 @@ import com.intellij.openapi.components.Service; import com.intellij.openapi.project.Project; -import com.zhongan.devpilot.enums.ModelServiceEnum; +import com.zhongan.devpilot.enums.LoginTypeEnum; import com.zhongan.devpilot.integrations.llms.aigateway.AIGatewayServiceProvider; -import com.zhongan.devpilot.integrations.llms.llama.LlamaServiceProvider; -import com.zhongan.devpilot.integrations.llms.ollama.OllamaServiceProvider; -import com.zhongan.devpilot.integrations.llms.openai.OpenAIServiceProvider; +import com.zhongan.devpilot.integrations.llms.trial.TrialServiceProvider; import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; @Service @@ -14,18 +12,14 @@ public final class LlmProviderFactory { public LlmProvider getLlmProvider(Project project) { var settings = DevPilotLlmSettingsState.getInstance(); - String selectedModel = settings.getSelectedModel(); - ModelServiceEnum modelServiceEnum = ModelServiceEnum.fromName(selectedModel); + var loginType = LoginTypeEnum.getLoginTypeEnum(settings.getLoginType()); - switch (modelServiceEnum) { - case OPENAI: - return project.getService(OpenAIServiceProvider.class); - case LLAMA: - return project.getService(LlamaServiceProvider.class); - case AIGATEWAY: + switch (loginType) { + case ZA: + case ZA_TI: return project.getService(AIGatewayServiceProvider.class); - case OLLAMA: - return project.getService(OllamaServiceProvider.class); + case WX: + return project.getService(TrialServiceProvider.class); } return project.getService(AIGatewayServiceProvider.class); diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/aigateway/AIGatewayServiceProvider.java b/src/main/java/com/zhongan/devpilot/integrations/llms/aigateway/AIGatewayServiceProvider.java index e327b477..5204ad8b 100644 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/aigateway/AIGatewayServiceProvider.java +++ b/src/main/java/com/zhongan/devpilot/integrations/llms/aigateway/AIGatewayServiceProvider.java @@ -1,24 +1,42 @@ package com.zhongan.devpilot.integrations.llms.aigateway; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.intellij.lang.Language; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.Service; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; -import com.zhongan.devpilot.DevPilotVersion; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.zhongan.devpilot.actions.notifications.DevPilotNotification; import com.zhongan.devpilot.enums.ModelTypeEnum; import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; import com.zhongan.devpilot.integrations.llms.LlmProvider; import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest; import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionResponse; import com.zhongan.devpilot.integrations.llms.entity.DevPilotFailedResponse; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotInstructCompletionRequest; import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; import com.zhongan.devpilot.integrations.llms.entity.DevPilotSuccessResponse; import com.zhongan.devpilot.settings.state.AIGatewaySettingsState; -import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; +import com.zhongan.devpilot.settings.state.LanguageSettingsState; import com.zhongan.devpilot.util.DevPilotMessageBundle; +import com.zhongan.devpilot.util.EditorUtils; +import com.zhongan.devpilot.util.GitUtil; +import com.zhongan.devpilot.util.LoginUtils; import com.zhongan.devpilot.util.OkhttpUtils; +import com.zhongan.devpilot.util.UserAgentUtils; import com.zhongan.devpilot.webview.model.MessageModel; import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; @@ -28,12 +46,17 @@ import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; +import okhttp3.Response; import okhttp3.sse.EventSource; +import static com.zhongan.devpilot.constant.DefaultConst.AI_GATEWAY_INSTRUCT_COMPLETION; +import static com.zhongan.devpilot.util.VirtualFileUtil.getRelativeFilePath; + @Service(Service.Level.PROJECT) public final class AIGatewayServiceProvider implements LlmProvider { - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private EventSource es; @@ -43,11 +66,18 @@ public final class AIGatewayServiceProvider implements LlmProvider { @Override public String chatCompletion(Project project, DevPilotChatCompletionRequest chatCompletionRequest, Consumer callback) { - var selectedModel = AIGatewaySettingsState.getInstance().getSelectedModel(); - var host = AIGatewaySettingsState.getInstance().getModelBaseHost(selectedModel); var service = project.getService(DevPilotChatToolWindowService.class); this.toolWindowService = service; + if (!LoginUtils.isLogin()) { + service.callErrorInfo("Chat completion failed: please login"); + DevPilotNotification.linkInfo("Please Login", "Account", LoginUtils.loginUrl()); + return ""; + } + + var selectedModel = AIGatewaySettingsState.getInstance().getSelectedModel(); + var host = AIGatewaySettingsState.getInstance().getModelBaseHost(selectedModel); + if (StringUtils.isEmpty(host)) { service.callErrorInfo("Chat completion failed: host is empty"); return ""; @@ -57,14 +87,28 @@ public String chatCompletion(Project project, DevPilotChatCompletionRequest chat chatCompletionRequest.setModel(modelTypeEnum.getCode()); try { - var request = new Request.Builder() - .url(host + "/devpilot/v1/chat/completions") - .header("User-Agent", parseUserAgent()) - .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) - .build(); + var requestBuilder = new Request.Builder() + .url(host + "/devpilot/v1/chat/completions") + .header("User-Agent", UserAgentUtils.buildUserAgent()) + .header("Auth-Type", LoginUtils.getLoginType()); + + if (isLatestUserContentContainsRepo(chatCompletionRequest)) { + String repoName = EditorUtils.getCurrentEditorRepositoryName(project); + if (repoName != null && GitUtil.isRepoEmbedded(repoName)) { + requestBuilder.header("Embedded-Repos-V2", repoName); + requestBuilder.header("X-B3-Language", LanguageSettingsState.getInstance().getLanguageIndex() == 1 ? "zh-CN" : "en-US"); + chatCompletionRequest.setModel(null); + } + } + var request = requestBuilder + .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) + .build(); + DevPilotNotification.debug(LoginUtils.getLoginType() + "---" + UserAgentUtils.buildUserAgent()); this.es = this.buildEventSource(request, service, callback); } catch (Exception e) { + DevPilotNotification.debug("Chat completion failed: " + e.getMessage()); + service.callErrorInfo("Chat completion failed: " + e.getMessage()); return ""; } @@ -93,10 +137,11 @@ public void restoreMessage(MessageModel messageModel) { this.resultModel = messageModel; } - private String parseUserAgent() { - // format: idea version|plugin version|uuid - return String.format("%s|%s|%s", DevPilotVersion.getIdeaVersion(), - DevPilotVersion.getDevPilotVersion(), DevPilotLlmSettingsState.getInstance().getUuid()); + @Override + public void handleNoAuth(DevPilotChatToolWindowService service) { + LoginUtils.logout(); + service.callErrorInfo("Chat completion failed: No auth, please login"); + DevPilotNotification.linkInfo("Please Login", "Account", LoginUtils.loginUrl()); } @Override @@ -111,29 +156,35 @@ public DevPilotChatCompletionResponse chatCompletionSync(DevPilotChatCompletionR var modelTypeEnum = ModelTypeEnum.fromName(selectedModel); chatCompletionRequest.setModel(modelTypeEnum.getCode()); - okhttp3.Response response; + Response response; try { + String requestBody = objectMapper.writeValueAsString(chatCompletionRequest); + DevPilotNotification.debug("Send Request :[" + requestBody + "]."); + var request = new Request.Builder() .url(host + "/devpilot/v1/chat/completions") - .header("User-Agent", parseUserAgent()) - .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) + .header("User-Agent", UserAgentUtils.buildUserAgent()) + .header("Auth-Type", LoginUtils.getLoginType()) + .post(RequestBody.create(requestBody, MediaType.parse("application/json"))) .build(); Call call = OkhttpUtils.getClient().newCall(request); response = call.execute(); } catch (Exception e) { + DevPilotNotification.debug("Chat completion failed: " + e.getMessage()); return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); } try { - return parseResult(chatCompletionRequest, response); + return parseCompletionsResult(chatCompletionRequest, response); } catch (IOException e) { + DevPilotNotification.debug("Chat completion failed: " + e.getMessage()); return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); } } - private DevPilotChatCompletionResponse parseResult(DevPilotChatCompletionRequest chatCompletionRequest, okhttp3.Response response) throws IOException { + private DevPilotChatCompletionResponse parseCompletionsResult(DevPilotChatCompletionRequest chatCompletionRequest, Response response) throws IOException { if (response == null) { return DevPilotChatCompletionResponse.failed(DevPilotMessageBundle.get("devpilot.chatWindow.response.null")); } @@ -151,6 +202,9 @@ private DevPilotChatCompletionResponse parseResult(DevPilotChatCompletionRequest chatCompletionRequest.getMessages().add(devPilotMessage); return DevPilotChatCompletionResponse.success(message.getContent()); + } else if (response.code() == 401) { + LoginUtils.logout(); + return DevPilotChatCompletionResponse.failed("Chat completion failed: Unauthorized, please login " + "sso" + ""); } else { return DevPilotChatCompletionResponse.failed(objectMapper.readValue(result, DevPilotFailedResponse.class) .getError() @@ -158,4 +212,125 @@ private DevPilotChatCompletionResponse parseResult(DevPilotChatCompletionRequest } } + @Override + public DevPilotMessage instructCompletion(DevPilotInstructCompletionRequest instructCompletionRequest) { + if (!LoginUtils.isLogin()) { + DevPilotNotification.infoAndAction("Instruct completion failed: please login", "", LoginUtils.loginUrl()); + return null; + } + + var selectedModel = AIGatewaySettingsState.getInstance().getSelectedModel(); + var host = AIGatewaySettingsState.getInstance().getModelBaseHost(selectedModel); + + if (StringUtils.isEmpty(host)) { + Logger.getInstance(getClass()).warn("Instruct completion failed: host is empty"); + return null; + } + + int offset = instructCompletionRequest.getOffset(); + Editor editor = instructCompletionRequest.getEditor(); + final Document[] document = new Document[1]; + final Language[] language = new Language[1]; + final VirtualFile[] virtualFile = new VirtualFile[1]; + + ApplicationManager.getApplication().runReadAction(() -> { + document[0] = editor.getDocument(); + language[0] = PsiDocumentManager.getInstance(editor.getProject()).getPsiFile(document[0]).getLanguage(); + virtualFile[0] = FileDocumentManager.getInstance().getFile(document[0]); + }); + + String text = document[0].getText(); + String relativePath = getRelativeFilePath(editor.getProject(), virtualFile[0]); + + Map map = new HashMap<>(); + map.put("document", text); + map.put("position", String.valueOf(offset)); + map.put("language", language[0].getID()); + map.put("filePath", relativePath); + map.put("completionType", instructCompletionRequest.getCompletionType()); + ObjectMapper objectMapper = new ObjectMapper(); + + Response response; + String json; + try { + json = objectMapper.writeValueAsString(map); + var request = new Request.Builder() + .url(host + AI_GATEWAY_INSTRUCT_COMPLETION) + .header("User-Agent", UserAgentUtils.buildUserAgent()) + .header("Auth-Type", LoginUtils.getLoginType()) + .header("X-B3-Language", LanguageSettingsState.getInstance().getLanguageIndex() == 1 ? "zh-CN" : "en-US") + .post(RequestBody.create(json, MediaType.parse("application/json"))) + .build(); + Call call = OkhttpUtils.getClient().newCall(request); + response = call.execute(); + } catch (Exception e) { + Logger.getInstance(getClass()).warn("Instruct completion failed: " + e.getMessage()); + return null; + } + + try { + return parseResponse(response); + } catch (Exception e) { + Logger.getInstance(getClass()).warn("Instruct completion failed: " + e.getMessage()); + return null; + } + } + + private DevPilotMessage parseResponse(Response response) { + DevPilotMessage devPilotMessage = null; + try (response) { + String responseBody = response.body().string(); + Gson gson = new Gson(); + devPilotMessage = gson.fromJson(responseBody, DevPilotMessage.class); + } catch (IOException e) { + Logger.getInstance(getClass()).warn("Parse completion response failed: " + e.getMessage()); + } + return devPilotMessage; + } + + private DevPilotMessage parseCompletionsResult(Response response) throws IOException { + if (response == null) { + return null; + } + + var result = Objects.requireNonNull(response.body()).string(); + + if (response.isSuccessful()) { + var map = objectMapper.readValue(result, Map.class); + var message = (List) map.get("choices"); + + var id = (String) map.get("id"); + var content = (String) message.get(0).get("text"); + + // multi chat message + var devPilotMessage = new DevPilotMessage(); + devPilotMessage.setId(id); + devPilotMessage.setRole("assistant"); + devPilotMessage.setContent(content); + return devPilotMessage; + } + DevPilotNotification.debug("SSO Type:" + LoginUtils.getLoginType() + ", Status Code:" + response.code() + "."); + if (response.code() == 401) { + LoginUtils.logout(); + } else { + DevPilotNotification.debug("Error message: [" + objectMapper.readValue(result, DevPilotFailedResponse.class).getError().getMessage() + "]."); + } + + return null; + } + + private Boolean isLatestUserContentContainsRepo(DevPilotChatCompletionRequest chatCompletionRequest) { + List messages = chatCompletionRequest.getMessages(); + for (int i = messages.size() - 1; i >= 0; i--) { + if (messages.get(i).getRole().equals("user")) { + String content = messages.get(i).getContent(); + if (content.startsWith("@repo")) { + messages.get(i).setContent(content.substring(5)); + return Boolean.TRUE; + } + return false; + } + } + return false; + } } diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotInstructCompletionRequest.java b/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotInstructCompletionRequest.java new file mode 100644 index 00000000..318a4e7d --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotInstructCompletionRequest.java @@ -0,0 +1,98 @@ +package com.zhongan.devpilot.integrations.llms.entity; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.intellij.openapi.editor.Editor; + +public class DevPilotInstructCompletionRequest { + + private Editor editor; + + private int offset; + + boolean stream = Boolean.FALSE; + + double temperature = 0L; + + @JsonProperty("max_tokens") + int maxTokens = 2000; + + int n = 1; + + String prompt = ""; + + String suffix = ""; + + String completionType = "inline"; + + public Editor getEditor() { + return editor; + } + + public void setEditor(Editor editor) { + this.editor = editor; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public boolean isStream() { + return stream; + } + + public void setStream(boolean stream) { + this.stream = stream; + } + + public double getTemperature() { + return temperature; + } + + public void setTemperature(double temperature) { + this.temperature = temperature; + } + + public int getMaxTokens() { + return maxTokens; + } + + public void setMaxTokens(int maxTokens) { + this.maxTokens = maxTokens; + } + + public int getN() { + return n; + } + + public void setN(int n) { + this.n = n; + } + + public String getPrompt() { + return prompt; + } + + public void setPrompt(String prompt) { + this.prompt = prompt; + } + + public String getSuffix() { + return suffix; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + public String getCompletionType() { + return completionType; + } + + public void setCompletionType(String completionType) { + this.completionType = completionType; + } +} diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotSuccessResponse.java b/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotSuccessResponse.java index 72ed1e23..01d613b0 100644 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotSuccessResponse.java +++ b/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotSuccessResponse.java @@ -1,9 +1,11 @@ package com.zhongan.devpilot.integrations.llms.entity; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +@JsonIgnoreProperties(ignoreUnknown = true) public class DevPilotSuccessResponse { private String id; @@ -66,6 +68,7 @@ public void setUsage(Usage usage) { this.usage = usage; } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Choice { private Integer index; @@ -101,6 +104,7 @@ public void setMessage(Message message) { } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Usage { @JsonProperty("completion_tokens") @@ -138,6 +142,7 @@ public void setTotalTokens(String totalTokens) { } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Message { private String role; diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotSuccessStreamingResponse.java b/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotSuccessStreamingResponse.java index 9c3b0585..a0b10cdc 100644 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotSuccessStreamingResponse.java +++ b/src/main/java/com/zhongan/devpilot/integrations/llms/entity/DevPilotSuccessStreamingResponse.java @@ -20,6 +20,8 @@ public class DevPilotSuccessStreamingResponse { private Usage usage; + private RagResp rag; + public String getId() { return id; } @@ -68,6 +70,14 @@ public void setUsage(Usage usage) { this.usage = usage; } + public RagResp getRag() { + return rag; + } + + public void setRag(RagResp rag) { + this.rag = rag; + } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Choice { @@ -103,6 +113,7 @@ public void setDelta(Message delta) { } } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Usage { @JsonProperty("completion_tokens") @@ -140,6 +151,7 @@ public void setTotalTokens(String totalTokens) { } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Message { private String role; @@ -164,4 +176,42 @@ public void setContent(String content) { } + @JsonIgnoreProperties(ignoreUnknown = true) + public static class RagResp { + + private String app; + + private List files; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public List getFiles() { + return files; + } + + public void setFiles(List files) { + this.files = files; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class RagFile { + + private String file; + + public String getFile() { + return file; + } + + public void setFile(String file) { + this.file = file; + } + } + } diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/OllamaModelListResponse.java b/src/main/java/com/zhongan/devpilot/integrations/llms/entity/OllamaModelListResponse.java deleted file mode 100644 index 739c322e..00000000 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/OllamaModelListResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.zhongan.devpilot.integrations.llms.entity; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.util.List; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class OllamaModelListResponse { - private List models; - - public List getModels() { - return models; - } - - public void setModels(List models) { - this.models = models; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Model { - - private String name; - - private long size; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public long getSize() { - return size; - } - - public void setSize(long size) { - this.size = size; - } - } - -} diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/OpenAIModelListResponse.java b/src/main/java/com/zhongan/devpilot/integrations/llms/entity/OpenAIModelListResponse.java deleted file mode 100644 index 448065e9..00000000 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/entity/OpenAIModelListResponse.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.zhongan.devpilot.integrations.llms.entity; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.util.List; - -@JsonIgnoreProperties(ignoreUnknown = true) -public class OpenAIModelListResponse { - - private List data; - - public List getData() { - return data; - } - - public void setData(List data) { - this.data = data; - } - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Data { - - private String id; - - private String object; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getObject() { - return object; - } - - public void setObject(String object) { - this.object = object; - } - } - -} diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/llama/LlamaServiceProvider.java b/src/main/java/com/zhongan/devpilot/integrations/llms/llama/LlamaServiceProvider.java deleted file mode 100644 index 9128bfd7..00000000 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/llama/LlamaServiceProvider.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.zhongan.devpilot.integrations.llms.llama; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.intellij.openapi.components.Service; -import com.intellij.openapi.project.Project; -import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; -import com.zhongan.devpilot.integrations.llms.LlmProvider; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionResponse; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotFailedResponse; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotSuccessResponse; -import com.zhongan.devpilot.settings.state.CodeLlamaSettingsState; -import com.zhongan.devpilot.util.DevPilotMessageBundle; -import com.zhongan.devpilot.util.OkhttpUtils; -import com.zhongan.devpilot.util.UserAgentUtils; -import com.zhongan.devpilot.webview.model.MessageModel; - -import java.io.IOException; -import java.util.Objects; -import java.util.function.Consumer; - -import org.apache.commons.lang3.StringUtils; - -import okhttp3.Call; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.sse.EventSource; - -@Service(Service.Level.PROJECT) -public final class LlamaServiceProvider implements LlmProvider { - - private final ObjectMapper objectMapper = new ObjectMapper(); - - private EventSource es; - - private DevPilotChatToolWindowService toolWindowService; - - private MessageModel resultModel = new MessageModel(); - - @Override - public String chatCompletion(Project project, DevPilotChatCompletionRequest chatCompletionRequest, Consumer callback) { - var host = CodeLlamaSettingsState.getInstance().getModelHost(); - var service = project.getService(DevPilotChatToolWindowService.class); - this.toolWindowService = service; - - if (StringUtils.isEmpty(host)) { - service.callErrorInfo("Chat completion failed: host is empty"); - return ""; - } - - var modelName = CodeLlamaSettingsState.getInstance().getModelName(); - - if (StringUtils.isEmpty(modelName)) { - service.callErrorInfo("Chat completion failed: code llama model name is empty"); - return ""; - } - - chatCompletionRequest.setModel(modelName); - - try { - var request = new Request.Builder() - .url(host + "/v1/chat/completions") - .header("User-Agent", UserAgentUtils.getUserAgent()) - .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) - .build(); - - this.es = this.buildEventSource(request, service, callback); - } catch (Exception e) { - service.callErrorInfo("Chat completion failed: " + e.getMessage()); - return ""; - } - - return ""; - } - - @Override - public void interruptSend() { - if (es != null) { - es.cancel(); - // remember the broken message - if (resultModel != null && !StringUtils.isEmpty(resultModel.getContent())) { - resultModel.setStreaming(false); - toolWindowService.addMessage(resultModel); - } - - toolWindowService.callWebView(); - // after interrupt, reset result model - resultModel = null; - } - } - - @Override - public void restoreMessage(MessageModel messageModel) { - this.resultModel = messageModel; - } - - @Override - public DevPilotChatCompletionResponse chatCompletionSync(DevPilotChatCompletionRequest chatCompletionRequest) { - var host = CodeLlamaSettingsState.getInstance().getModelHost(); - - if (StringUtils.isEmpty(host)) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: host is empty"); - } - - var modelName = CodeLlamaSettingsState.getInstance().getModelName(); - - if (StringUtils.isEmpty(modelName)) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: code llama model name is empty"); - } - - chatCompletionRequest.setModel(modelName); - - okhttp3.Response response; - - try { - var request = new Request.Builder() - .url(host + "/v1/chat/completions") - .header("User-Agent", UserAgentUtils.getUserAgent()) - .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) - .build(); - - Call call = OkhttpUtils.getClient().newCall(request); - response = call.execute(); - } catch (Exception e) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); - } - - try { - return parseResult(chatCompletionRequest, response); - } catch (IOException e) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); - } - } - - private DevPilotChatCompletionResponse parseResult(DevPilotChatCompletionRequest chatCompletionRequest, okhttp3.Response response) throws IOException { - if (response == null) { - return DevPilotChatCompletionResponse.failed(DevPilotMessageBundle.get("devpilot.chatWindow.response.null")); - } - - var result = Objects.requireNonNull(response.body()).string(); - - if (response.isSuccessful()) { - var message = objectMapper.readValue(result, DevPilotSuccessResponse.class) - .getChoices() - .get(0) - .getMessage(); - var devPilotMessage = new DevPilotMessage(); - devPilotMessage.setRole("assistant"); - devPilotMessage.setContent(message.getContent()); - chatCompletionRequest.getMessages().add(devPilotMessage); - return DevPilotChatCompletionResponse.success(message.getContent()); - - } else { - return DevPilotChatCompletionResponse.failed(objectMapper.readValue(result, DevPilotFailedResponse.class) - .getError() - .getMessage()); - } - } - -} diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/ollama/OllamaServiceProvider.java b/src/main/java/com/zhongan/devpilot/integrations/llms/ollama/OllamaServiceProvider.java deleted file mode 100644 index 76579fec..00000000 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/ollama/OllamaServiceProvider.java +++ /dev/null @@ -1,194 +0,0 @@ -package com.zhongan.devpilot.integrations.llms.ollama; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.intellij.openapi.components.Service; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.project.Project; -import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; -import com.zhongan.devpilot.integrations.llms.LlmProvider; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionResponse; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotFailedResponse; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotSuccessResponse; -import com.zhongan.devpilot.integrations.llms.entity.OllamaModelListResponse; -import com.zhongan.devpilot.settings.state.OllamaSettingsState; -import com.zhongan.devpilot.util.DevPilotMessageBundle; -import com.zhongan.devpilot.util.JsonUtils; -import com.zhongan.devpilot.util.OkhttpUtils; -import com.zhongan.devpilot.util.UserAgentUtils; -import com.zhongan.devpilot.webview.model.MessageModel; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -import org.apache.commons.lang3.StringUtils; - -import okhttp3.Call; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.sse.EventSource; - -@Service(Service.Level.PROJECT) -public final class OllamaServiceProvider implements LlmProvider { - private static final Logger log = Logger.getInstance(OllamaServiceProvider.class); - - private final ObjectMapper objectMapper = new ObjectMapper(); - - private EventSource es; - - private DevPilotChatToolWindowService toolWindowService; - - private MessageModel resultModel = new MessageModel(); - - @Override - public String chatCompletion(Project project, DevPilotChatCompletionRequest chatCompletionRequest, Consumer callback) { - var host = OllamaSettingsState.getInstance().getModelHost(); - var modelName = OllamaSettingsState.getInstance().getModelName(); - var service = project.getService(DevPilotChatToolWindowService.class); - this.toolWindowService = service; - - if (StringUtils.isEmpty(host)) { - service.callErrorInfo("Chat completion failed: host is empty"); - return ""; - } - - if (StringUtils.isEmpty(modelName)) { - service.callErrorInfo("Chat completion failed: ollama model name is empty"); - return ""; - } - - if (host.endsWith("/")) { - host = host.substring(0, host.length() - 1); - } - - chatCompletionRequest.setModel(modelName); - - try { - var request = new Request.Builder() - .url(host + "/v1/chat/completions") - .header("User-Agent", UserAgentUtils.getUserAgent()) - .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) - .build(); - - this.es = this.buildEventSource(request, service, callback); - } catch (Exception e) { - service.callErrorInfo("Chat completion failed: " + e.getMessage()); - return ""; - } - - return ""; - } - - @Override - public void interruptSend() { - if (es != null) { - es.cancel(); - // remember the broken message - if (resultModel != null && !StringUtils.isEmpty(resultModel.getContent())) { - resultModel.setStreaming(false); - toolWindowService.addMessage(resultModel); - } - - toolWindowService.callWebView(); - // after interrupt, reset result model - resultModel = null; - } - } - - @Override - public List listModels(String host, String apiKey) { - if (host.endsWith("/")) { - host = host.substring(0, host.length() - 1); - } - List modelList = new ArrayList<>(); - try { - var request = new Request.Builder() - .get() - .url(host + "/api/tags") - .build(); - Call call = OkhttpUtils.getClient().newCall(request); - okhttp3.Response response = call.execute(); - if (response.isSuccessful()) { - var result = Objects.requireNonNull(response.body()).string(); - var modelListResponse = JsonUtils.fromJson(result, OllamaModelListResponse.class); - if (modelListResponse != null) { - for (OllamaModelListResponse.Model model : modelListResponse.getModels()) { - modelList.add(model.getName()); - } - } - } - response.close(); - } catch (Exception ex) { - log.error("ollama list models error", ex); - } - return modelList; - } - - @Override - public DevPilotChatCompletionResponse chatCompletionSync(DevPilotChatCompletionRequest chatCompletionRequest) { - var host = OllamaSettingsState.getInstance().getModelHost(); - var modelName = OllamaSettingsState.getInstance().getModelName(); - - if (StringUtils.isEmpty(host)) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: host is empty"); - } - - if (StringUtils.isEmpty(modelName)) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: ollama model name is empty"); - } - - chatCompletionRequest.setModel(modelName); - - okhttp3.Response response; - - try { - var request = new Request.Builder() - .url(host + "/v1/chat/completions") - .header("User-Agent", UserAgentUtils.getUserAgent()) - .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) - .build(); - - Call call = OkhttpUtils.getClient().newCall(request); - response = call.execute(); - } catch (Exception e) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); - } - - try { - return parseResult(chatCompletionRequest, response); - } catch (IOException e) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); - } - } - - private DevPilotChatCompletionResponse parseResult(DevPilotChatCompletionRequest chatCompletionRequest, okhttp3.Response response) throws IOException { - if (response == null) { - return DevPilotChatCompletionResponse.failed(DevPilotMessageBundle.get("devpilot.chatWindow.response.null")); - } - - var result = Objects.requireNonNull(response.body()).string(); - - if (response.isSuccessful()) { - var message = objectMapper.readValue(result, DevPilotSuccessResponse.class) - .getChoices() - .get(0) - .getMessage(); - var devPilotMessage = new DevPilotMessage(); - devPilotMessage.setRole("assistant"); - devPilotMessage.setContent(message.getContent()); - chatCompletionRequest.getMessages().add(devPilotMessage); - return DevPilotChatCompletionResponse.success(message.getContent()); - - } else { - return DevPilotChatCompletionResponse.failed(objectMapper.readValue(result, DevPilotFailedResponse.class) - .getError() - .getMessage()); - } - } - -} diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/openai/OpenAIServiceProvider.java b/src/main/java/com/zhongan/devpilot/integrations/llms/openai/OpenAIServiceProvider.java deleted file mode 100644 index c12a6129..00000000 --- a/src/main/java/com/zhongan/devpilot/integrations/llms/openai/OpenAIServiceProvider.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.zhongan.devpilot.integrations.llms.openai; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.intellij.openapi.components.Service; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.project.Project; -import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; -import com.zhongan.devpilot.integrations.llms.LlmProvider; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionResponse; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotFailedResponse; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; -import com.zhongan.devpilot.integrations.llms.entity.DevPilotSuccessResponse; -import com.zhongan.devpilot.integrations.llms.entity.OpenAIModelListResponse; -import com.zhongan.devpilot.settings.state.OpenAISettingsState; -import com.zhongan.devpilot.util.DevPilotMessageBundle; -import com.zhongan.devpilot.util.JsonUtils; -import com.zhongan.devpilot.util.OkhttpUtils; -import com.zhongan.devpilot.util.UserAgentUtils; -import com.zhongan.devpilot.webview.model.MessageModel; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -import org.apache.commons.lang3.StringUtils; - -import okhttp3.Call; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.sse.EventSource; - -@Service(Service.Level.PROJECT) -public final class OpenAIServiceProvider implements LlmProvider { - - private static final Logger log = Logger.getInstance(OpenAIServiceProvider.class); - - private final ObjectMapper objectMapper = new ObjectMapper(); - - private EventSource es; - - private DevPilotChatToolWindowService toolWindowService; - - private MessageModel resultModel = new MessageModel(); - - @Override - public String chatCompletion(Project project, DevPilotChatCompletionRequest chatCompletionRequest, Consumer callback) { - var host = OpenAISettingsState.getInstance().getModelHost(); - var apiKey = OpenAISettingsState.getInstance().getPrivateKey(); - var service = project.getService(DevPilotChatToolWindowService.class); - this.toolWindowService = service; - - if (StringUtils.isEmpty(host)) { - service.callErrorInfo("Chat completion failed: host is empty"); - return ""; - } - - if (StringUtils.isEmpty(apiKey)) { - service.callErrorInfo("Chat completion failed: api key is empty"); - return ""; - } - - var modelName = OpenAISettingsState.getInstance().getModelName(); - - if (StringUtils.isEmpty(modelName)) { - service.callErrorInfo("Chat completion failed: openai model name is empty"); - return ""; - } - - chatCompletionRequest.setModel(modelName); - - try { - var request = new Request.Builder() - .url(host + "/v1/chat/completions") - .header("User-Agent", UserAgentUtils.getUserAgent()) - .header("Authorization", "Bearer " + apiKey) - .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) - .build(); - - this.es = this.buildEventSource(request, service, callback); - } catch (Exception e) { - service.callErrorInfo("Chat completion failed: " + e.getMessage()); - return ""; - } - - return ""; - } - - @Override - public void interruptSend() { - if (es != null) { - es.cancel(); - // remember the broken message - if (resultModel != null && !StringUtils.isEmpty(resultModel.getContent())) { - resultModel.setStreaming(false); - toolWindowService.addMessage(resultModel); - } - - toolWindowService.callWebView(); - // after interrupt, reset result model - resultModel = null; - } - } - - @Override - public List listModels(String host, String apiKey) { - if (host.endsWith("/")) { - host = host.substring(0, host.length() - 1); - } - List modelList = new ArrayList<>(); - try { - var request = new Request.Builder() - .header("User-Agent", UserAgentUtils.getUserAgent()) - .header("Authorization", "Bearer " + apiKey) - .get() - .url(host + "/v1/models") - .build(); - Call call = OkhttpUtils.getClient().newCall(request); - okhttp3.Response response = call.execute(); - if (response.isSuccessful()) { - var result = Objects.requireNonNull(response.body()).string(); - var modelListResponse = JsonUtils.fromJson(result, OpenAIModelListResponse.class); - if (modelListResponse != null) { - for (OpenAIModelListResponse.Data model : modelListResponse.getData()) { - modelList.add(model.getId()); - } - } - } - response.close(); - } catch (Exception ex) { - log.error("openAI list models error", ex); - } - return modelList; - } - - @Override - public DevPilotChatCompletionResponse chatCompletionSync(DevPilotChatCompletionRequest chatCompletionRequest) { - var host = OpenAISettingsState.getInstance().getModelHost(); - var apiKey = OpenAISettingsState.getInstance().getPrivateKey(); - - if (StringUtils.isEmpty(host)) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: host is empty"); - } - - if (StringUtils.isEmpty(apiKey)) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: api key is empty"); - } - - var modelName = OpenAISettingsState.getInstance().getModelName(); - - if (StringUtils.isEmpty(modelName)) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: openai model name is empty"); - } - - chatCompletionRequest.setModel(modelName); - - okhttp3.Response response; - - try { - var request = new Request.Builder() - .url(host + "/v1/chat/completions") - .header("User-Agent", UserAgentUtils.getUserAgent()) - .header("Authorization", "Bearer " + apiKey) - .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) - .build(); - - Call call = OkhttpUtils.getClient().newCall(request); - response = call.execute(); - } catch (Exception e) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); - } - - try { - return parseResult(chatCompletionRequest, response); - } catch (IOException e) { - return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); - } - } - - private DevPilotChatCompletionResponse parseResult(DevPilotChatCompletionRequest chatCompletionRequest, okhttp3.Response response) throws IOException { - if (response == null) { - return DevPilotChatCompletionResponse.failed(DevPilotMessageBundle.get("devpilot.chatWindow.response.null")); - } - - var result = Objects.requireNonNull(response.body()).string(); - - if (response.isSuccessful()) { - var message = objectMapper.readValue(result, DevPilotSuccessResponse.class) - .getChoices() - .get(0) - .getMessage(); - var devPilotMessage = new DevPilotMessage(); - devPilotMessage.setRole("assistant"); - devPilotMessage.setContent(message.getContent()); - chatCompletionRequest.getMessages().add(devPilotMessage); - return DevPilotChatCompletionResponse.success(message.getContent()); - - } else { - return DevPilotChatCompletionResponse.failed(objectMapper.readValue(result, DevPilotFailedResponse.class) - .getError() - .getMessage()); - } - } - -} diff --git a/src/main/java/com/zhongan/devpilot/integrations/llms/trial/TrialServiceProvider.java b/src/main/java/com/zhongan/devpilot/integrations/llms/trial/TrialServiceProvider.java new file mode 100644 index 00000000..274ccdc3 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/integrations/llms/trial/TrialServiceProvider.java @@ -0,0 +1,243 @@ +package com.zhongan.devpilot.integrations.llms.trial; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.intellij.lang.Language; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.Service; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.zhongan.devpilot.actions.notifications.DevPilotNotification; +import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; +import com.zhongan.devpilot.integrations.llms.LlmProvider; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionResponse; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotFailedResponse; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotInstructCompletionRequest; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotSuccessResponse; +import com.zhongan.devpilot.settings.state.LanguageSettingsState; +import com.zhongan.devpilot.util.DevPilotMessageBundle; +import com.zhongan.devpilot.util.LoginUtils; +import com.zhongan.devpilot.util.OkhttpUtils; +import com.zhongan.devpilot.util.UserAgentUtils; +import com.zhongan.devpilot.webview.model.MessageModel; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; + +import org.apache.commons.lang3.StringUtils; + +import okhttp3.Call; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.sse.EventSource; + +import static com.zhongan.devpilot.constant.DefaultConst.AI_GATEWAY_INSTRUCT_COMPLETION; +import static com.zhongan.devpilot.constant.DefaultConst.TRIAL_DEFAULT_HOST; +import static com.zhongan.devpilot.constant.DefaultConst.TRIAL_DEFAULT_MODEL; +import static com.zhongan.devpilot.util.VirtualFileUtil.getRelativeFilePath; + +@Service(Service.Level.PROJECT) +public final class TrialServiceProvider implements LlmProvider { + private final ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + private EventSource es; + + private DevPilotChatToolWindowService toolWindowService; + + private MessageModel resultModel = new MessageModel(); + + @Override + public String chatCompletion(Project project, DevPilotChatCompletionRequest chatCompletionRequest, Consumer callback) { + var service = project.getService(DevPilotChatToolWindowService.class); + this.toolWindowService = service; + + if (!LoginUtils.isLogin()) { + service.callErrorInfo("Chat completion failed: please login"); + DevPilotNotification.linkInfo("Please Login", "Account", LoginUtils.loginUrl()); + return ""; + } + + chatCompletionRequest.setModel(TRIAL_DEFAULT_MODEL); + + try { + var request = new Request.Builder() + .url(TRIAL_DEFAULT_HOST + "/v1/chat/completions") + .header("User-Agent", UserAgentUtils.buildUserAgent()) + .header("Auth-Type", "wx") + .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) + .build(); + + this.es = this.buildEventSource(request, service, callback); + } catch (Exception e) { + service.callErrorInfo("Chat completion failed: " + e.getMessage()); + return ""; + } + + return ""; + } + + @Override + public DevPilotChatCompletionResponse chatCompletionSync(DevPilotChatCompletionRequest chatCompletionRequest) { + if (!LoginUtils.isLogin()) { + return DevPilotChatCompletionResponse.failed("Chat completion failed: please login Wechat Login"); + } + + chatCompletionRequest.setModel(TRIAL_DEFAULT_MODEL); + + Response response; + + try { + var request = new Request.Builder() + .url(TRIAL_DEFAULT_HOST + "/v1/chat/completions") + .header("User-Agent", UserAgentUtils.buildUserAgent()) + .header("Auth-Type", "wx") + .post(RequestBody.create(objectMapper.writeValueAsString(chatCompletionRequest), MediaType.parse("application/json"))) + .build(); + + var call = OkhttpUtils.getClient().newCall(request); + response = call.execute(); + } catch (Exception e) { + return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); + } + + try { + return parseResult(chatCompletionRequest, response); + } catch (Exception e) { + return DevPilotChatCompletionResponse.failed("Chat completion failed: " + e.getMessage()); + } + } + + @Override + public DevPilotMessage instructCompletion(DevPilotInstructCompletionRequest instructCompletionRequest) { + if (!LoginUtils.isLogin()) { + DevPilotNotification.infoAndAction("Instruct completion failed: please login", "", LoginUtils.loginUrl()); + return null; + } + + int offset = instructCompletionRequest.getOffset(); + Editor editor = instructCompletionRequest.getEditor(); + final Document[] document = new Document[1]; + final Language[] language = new Language[1]; + final VirtualFile[] virtualFile = new VirtualFile[1]; + + ApplicationManager.getApplication().runReadAction(() -> { + document[0] = editor.getDocument(); + language[0] = PsiDocumentManager.getInstance(editor.getProject()).getPsiFile(document[0]).getLanguage(); + virtualFile[0] = FileDocumentManager.getInstance().getFile(document[0]); + }); + + String text = document[0].getText(); + String relativePath = getRelativeFilePath(editor.getProject(), virtualFile[0]); + + Map map = new HashMap<>(); + map.put("document", text); + map.put("position", String.valueOf(offset)); + map.put("language", language[0].getID()); + map.put("filePath", relativePath); + map.put("completionType", instructCompletionRequest.getCompletionType()); + ObjectMapper objectMapper = new ObjectMapper(); + + Response response; + String json; + try { + json = objectMapper.writeValueAsString(map); + var request = new Request.Builder() + .url(TRIAL_DEFAULT_HOST + AI_GATEWAY_INSTRUCT_COMPLETION) + .header("User-Agent", UserAgentUtils.buildUserAgent()) + .header("Auth-Type", LoginUtils.getLoginType()) + .header("X-B3-Language", LanguageSettingsState.getInstance().getLanguageIndex() == 1 ? "zh-CN" : "en-US") + .post(RequestBody.create(json, MediaType.parse("application/json"))) + .build(); + Call call = OkhttpUtils.getClient().newCall(request); + response = call.execute(); + } catch (Exception e) { + Logger.getInstance(getClass()).warn("Instruct completion failed: " + e.getMessage()); + return null; + } + + try { + return parseResponse(response); + } catch (Exception e) { + Logger.getInstance(getClass()).warn("Instruct completion failed: " + e.getMessage()); + return null; + } + } + + private DevPilotMessage parseResponse(Response response) { + DevPilotMessage devPilotMessage = null; + try (response) { + String responseBody = response.body().string(); + Gson gson = new Gson(); + devPilotMessage = gson.fromJson(responseBody, DevPilotMessage.class); + } catch (IOException e) { + Logger.getInstance(getClass()).warn("Parse completion response failed: " + e.getMessage()); + } + return devPilotMessage; + } + + @Override + public void interruptSend() { + if (es != null) { + es.cancel(); + // remember the broken message + if (resultModel != null && !StringUtils.isEmpty(resultModel.getContent())) { + resultModel.setStreaming(false); + toolWindowService.addMessage(resultModel); + } + + toolWindowService.callWebView(); + // after interrupt, reset result model + resultModel = null; + } + } + + @Override + public void handleNoAuth(DevPilotChatToolWindowService service) { + LoginUtils.logout(); + service.callErrorInfo("Chat completion failed: No auth, please login"); + DevPilotNotification.linkInfo("Please Login", "Account", LoginUtils.loginUrl()); + } + + private DevPilotChatCompletionResponse parseResult(DevPilotChatCompletionRequest chatCompletionRequest, Response response) throws IOException { + + if (response == null) { + return DevPilotChatCompletionResponse.failed(DevPilotMessageBundle.get("devpilot.chatWindow.response.null")); + } + + var result = Objects.requireNonNull(response.body()).string(); + + if (response.isSuccessful()) { + var message = objectMapper.readValue(result, DevPilotSuccessResponse.class) + .getChoices() + .get(0) + .getMessage(); + var devPilotMessage = new DevPilotMessage(); + devPilotMessage.setRole("assistant"); + devPilotMessage.setContent(message.getContent()); + chatCompletionRequest.getMessages().add(devPilotMessage); + return DevPilotChatCompletionResponse.success(message.getContent()); + + } else if (response.code() == 401) { + LoginUtils.logout(); + return DevPilotChatCompletionResponse.failed("Chat completion failed: Unauthorized, please login Wechat Login"); + } else { + return DevPilotChatCompletionResponse.failed(objectMapper.readValue(result, DevPilotFailedResponse.class) + .getError() + .getMessage()); + } + } +} diff --git a/src/main/java/com/zhongan/devpilot/listener/DevPilotFileEditorListener.java b/src/main/java/com/zhongan/devpilot/listener/DevPilotFileEditorListener.java new file mode 100644 index 00000000..3b79e014 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/listener/DevPilotFileEditorListener.java @@ -0,0 +1,53 @@ +package com.zhongan.devpilot.listener; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.fileEditor.FileEditorManagerEvent; +import com.intellij.openapi.fileEditor.FileEditorManagerListener; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; +import com.zhongan.devpilot.util.GitUtil; +import com.zhongan.devpilot.util.LoginUtils; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.jetbrains.annotations.NotNull; + +import static com.zhongan.devpilot.util.GitUtil.isRepoEmbedded; + +public class DevPilotFileEditorListener implements FileEditorManagerListener { + + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + + public static void registerListener() { + ApplicationManager.getApplication() + .getMessageBus().connect().subscribe(FILE_EDITOR_MANAGER, new DevPilotFileEditorListener()); + } + + @Override + public void selectionChanged(@NotNull FileEditorManagerEvent event) { + if (!LoginUtils.isLogin() || LoginUtils.getLoginType().equals("wx")) { + return; + } + + VirtualFile file = event.getNewEditor() != null ? event.getNewEditor().getFile() : null; + if (file == null) { + return; + } + executorService.execute(() -> handleSelectionChanged(file, event.getManager().getProject())); + } + + private void handleSelectionChanged(VirtualFile file, Project project) { + DevPilotChatToolWindowService service = project.getService(DevPilotChatToolWindowService.class); + + String repoName = GitUtil.getRepoNameFromFile(project, file); + if (repoName == null) { + service.presentRepoCodeEmbeddedState(false, null); + return; + } + + Boolean repoEmbedded = isRepoEmbedded(repoName); + service.presentRepoCodeEmbeddedState(repoEmbedded, repoName); + } +} diff --git a/src/main/java/com/zhongan/devpilot/rest/LoginAuthCallbackRestService.java b/src/main/java/com/zhongan/devpilot/rest/LoginAuthCallbackRestService.java new file mode 100644 index 00000000..befb9a14 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/rest/LoginAuthCallbackRestService.java @@ -0,0 +1,91 @@ +package com.zhongan.devpilot.rest; + +import com.zhongan.devpilot.enums.LoginTypeEnum; +import com.zhongan.devpilot.enums.ZaSsoEnum; +import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; +import com.zhongan.devpilot.util.CallbackUtils; +import com.zhongan.devpilot.util.LoginUtils; +import com.zhongan.devpilot.util.RestServiceUtils; +import com.zhongan.devpilot.util.WxAuthUtils; +import com.zhongan.devpilot.util.ZaSsoUtils; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.QueryStringDecoder; + +import java.io.IOException; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.ide.RestService; + +public class LoginAuthCallbackRestService extends RestService { + @Nullable + @Override + public String execute(@NotNull QueryStringDecoder queryStringDecoder, @NotNull FullHttpRequest fullHttpRequest, @NotNull ChannelHandlerContext channelHandlerContext) throws IOException { + var token = RestServiceUtils.getParameter(queryStringDecoder, "token"); + var scope = RestServiceUtils.getParameter(queryStringDecoder, "scope"); + + if (token == null || scope == null) { + sendResponse(fullHttpRequest, channelHandlerContext, CallbackUtils.buildFailResponse()); + return "token or scope should not null"; + } + + var user = RestServiceUtils.parseToken(token); + + if (user == null) { + sendResponse(fullHttpRequest, channelHandlerContext, CallbackUtils.buildFailResponse()); + return "user analysis fail"; + } + + LoginTypeEnum loginType; + String username; + + switch (scope) { + case "gzh": + loginType = LoginTypeEnum.WX; + username = user.getNickname(); + WxAuthUtils.login(user.getToken(), user.getNickname(), user.getOpenid()); + break; + case "za": + loginType = LoginTypeEnum.ZA; + username = user.getUsername(); + ZaSsoUtils.login(ZaSsoEnum.ZA, user.getToken(), user.getUsername()); + break; + case "zati": + loginType = LoginTypeEnum.ZA_TI; + username = user.getUsername(); + ZaSsoUtils.login(ZaSsoEnum.ZA_TI, user.getToken(), user.getUsername()); + break; + default: + sendResponse(fullHttpRequest, channelHandlerContext, CallbackUtils.buildFailResponse()); + return "scope invalid"; + } + + var setting = DevPilotLlmSettingsState.getInstance(); + setting.setLoginType(loginType.getType()); + + LoginUtils.changeLoginStatus(true); + + sendResponse(fullHttpRequest, channelHandlerContext, CallbackUtils.buildSuccessResponse()); + return null; + } + + @Override + protected boolean isPrefixlessAllowed() { + return true; + } + + @NotNull + @Override + protected OriginCheckResult isOriginAllowed(@NotNull HttpRequest request) { + return OriginCheckResult.ALLOW; + } + + @NotNull + @Override + protected String getServiceName() { + return "login/auth/callback"; + } +} diff --git a/src/main/java/com/zhongan/devpilot/settings/DevPilotConfigForm.java b/src/main/java/com/zhongan/devpilot/settings/DevPilotConfigForm.java deleted file mode 100644 index 49e7c542..00000000 --- a/src/main/java/com/zhongan/devpilot/settings/DevPilotConfigForm.java +++ /dev/null @@ -1,384 +0,0 @@ -package com.zhongan.devpilot.settings; - -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.ui.ComboBox; -import com.intellij.ui.components.JBTextField; -import com.intellij.util.ui.FormBuilder; -import com.intellij.util.ui.JBUI; -import com.intellij.util.ui.UI; -import com.zhongan.devpilot.enums.ModelServiceEnum; -import com.zhongan.devpilot.enums.ModelTypeEnum; -import com.zhongan.devpilot.integrations.llms.LlmProvider; -import com.zhongan.devpilot.integrations.llms.ollama.OllamaServiceProvider; -import com.zhongan.devpilot.integrations.llms.openai.OpenAIServiceProvider; -import com.zhongan.devpilot.settings.state.AIGatewaySettingsState; -import com.zhongan.devpilot.settings.state.CodeLlamaSettingsState; -import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; -import com.zhongan.devpilot.settings.state.LanguageSettingsState; -import com.zhongan.devpilot.settings.state.OllamaSettingsState; -import com.zhongan.devpilot.settings.state.OpenAISettingsState; -import com.zhongan.devpilot.util.DevPilotMessageBundle; - -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.List; - -import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JComponent; -import javax.swing.JPanel; - -public class DevPilotConfigForm { - - private static final String CUSTOM_MODEL = "custom"; - - private final JPanel comboBoxPanel; - - private final ComboBox modelComboBox; - - private final JPanel openAIServicePanel; - - private final JBTextField openAIBaseHostField; - - private final JBTextField openAIKeyField; - - private final ComboBox openAIModelNameComboBox; - - private final JButton openAIRefreshModelBtn; - - private final JBTextField openAICustomModelNameField; - - private final JPanel aiGatewayServicePanel; - - private final JBTextField aiGatewayBaseHostField; - - private final ComboBox aiGatewayModelComboBox; - - private final JPanel codeLlamaServicePanel; - - private final JBTextField codeLlamaBaseHostField; - - private final JBTextField codeLlamaModelNameField; - - /** - * ollama panel - */ - private final JPanel ollamaServicePanel; - - private final JBTextField ollamaBaseHostField; - - private final JButton ollamaRefreshModelBtn; - - private final ComboBox ollamaModelComboBox; - - private Integer index; - - public DevPilotConfigForm() { - var devPilotSettings = DevPilotLlmSettingsState.getInstance(); - - var selectedModel = devPilotSettings.getSelectedModel(); - ModelServiceEnum selectedEnum = ModelServiceEnum.fromName(selectedModel); - - var openAISettings = OpenAISettingsState.getInstance(); - openAIBaseHostField = new JBTextField(openAISettings.getModelHost(), 30); - openAIKeyField = new JBTextField(openAISettings.getPrivateKey(), 30); - openAICustomModelNameField = new JBTextField(openAISettings.getCustomModelName(), 15); - var modelName = openAISettings.getModelName(); - openAICustomModelNameField.setEnabled(CUSTOM_MODEL.equals(modelName)); - openAIModelNameComboBox = new ComboBox<>(); - openAIModelNameComboBox.addItemListener(e -> { - var selected = (String) e.getItem(); - openAICustomModelNameField.setEnabled(CUSTOM_MODEL.equals(selected)); - }); - openAIRefreshModelBtn = new JButton(DevPilotMessageBundle.get("devpilot.settings.service.refreshModelList")); - openAIRefreshModelBtn.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - super.mouseClicked(e); - refreshOpenAIModels(); - } - }); - - openAIServicePanel = createOpenAIServicePanel(); - - var aiGatewaySettings = AIGatewaySettingsState.getInstance(); - var aiGatewayModel = aiGatewaySettings.getSelectedModel(); - var aiGatewayModelEnum = ModelTypeEnum.fromName(aiGatewayModel); - var host = aiGatewaySettings.getModelBaseHost(aiGatewayModel); - aiGatewayBaseHostField = new JBTextField(host, 30); - var modelTypeEnumComboBox = new ComboBox<>(ModelTypeEnum.values()); - modelTypeEnumComboBox.setSelectedItem(aiGatewayModelEnum); - modelTypeEnumComboBox.addItemListener(e -> { - var selected = (ModelTypeEnum) e.getItem(); - aiGatewayBaseHostField.setText(aiGatewaySettings.getModelBaseHost(selected.getName())); - }); - aiGatewayModelComboBox = modelTypeEnumComboBox; - aiGatewayServicePanel = createAIGatewayServicePanel(); - - var codeLlamaSettings = CodeLlamaSettingsState.getInstance(); - codeLlamaBaseHostField = new JBTextField(codeLlamaSettings.getModelHost(), 30); - codeLlamaModelNameField = new JBTextField(codeLlamaSettings.getModelName(), 30); - codeLlamaServicePanel = createCodeLlamaServicePanel(); - - var ollamaSettingsState = OllamaSettingsState.getInstance(); - ollamaBaseHostField = new JBTextField(ollamaSettingsState.getModelHost(), 30); - ollamaRefreshModelBtn = new JButton(DevPilotMessageBundle.get("devpilot.settings.service.refreshModelList")); - ollamaModelComboBox = new ComboBox<>(); - ollamaRefreshModelBtn.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - super.mouseClicked(e); - refreshOllamaModels(); - } - }); - - ollamaServicePanel = createOllamaServicePanel(); - - panelShow(selectedEnum); - - var combo = new ComboBox<>(ModelServiceEnum.values()); - combo.setSelectedItem(selectedEnum); - combo.addItemListener(e -> { - var selected = (ModelServiceEnum) e.getItem(); - panelShow(selected); - }); - - modelComboBox = combo; - comboBoxPanel = createOpenAIServiceSectionPanel( - DevPilotMessageBundle.get("devpilot.settings.service.modelTypeLabel"), modelComboBox); - - var instance = LanguageSettingsState.getInstance(); - index = instance.getLanguageIndex(); - - refreshOpenAIModels(); - refreshOllamaModels(); - } - - public void refreshOllamaModels() { - ollamaModelComboBox.removeAllItems(); - String host = ollamaBaseHostField.getText(); - if (null == host || "".equals(host)) { - return; - } - - LlmProvider llmProvider = ApplicationManager.getApplication().getService(OllamaServiceProvider.class); - List modelList = llmProvider.listModels(host, ""); - for (String modelName : modelList) { - ollamaModelComboBox.addItem(modelName); - } - ollamaModelComboBox.setSelectedItem(OllamaSettingsState.getInstance().getModelName()); - } - - public void refreshOpenAIModels() { - openAIModelNameComboBox.removeAllItems(); - String host = openAIBaseHostField.getText(); - String apiKey = openAIKeyField.getText(); - if (null == host || "".equals(host) || null == apiKey || "".equals(apiKey)) { - openAIModelNameComboBox.addItem(CUSTOM_MODEL); - return; - } - - LlmProvider llmProvider = ApplicationManager.getApplication().getService(OpenAIServiceProvider.class); - List modelList = llmProvider.listModels(host, apiKey); - for (String modelName : modelList) { - openAIModelNameComboBox.addItem(modelName); - } - openAIModelNameComboBox.addItem(CUSTOM_MODEL); - openAIModelNameComboBox.setSelectedItem(OpenAISettingsState.getInstance().getModelName()); - } - - public JComponent getForm() { - var form = FormBuilder.createFormBuilder() - .addComponent(comboBoxPanel) - .addComponent(openAIServicePanel) - .addComponent(codeLlamaServicePanel) - .addComponent(aiGatewayServicePanel) - .addComponent(ollamaServicePanel) - .getPanel(); - form.setBorder(JBUI.Borders.emptyLeft(16)); - return form; - } - - public JPanel createLanguageSectionPanel(Integer languageIndex) { - var comboBox = new ComboBox<>(); - comboBox.addItem("English"); - comboBox.addItem("中文"); - comboBox.setSelectedIndex(languageIndex); - - comboBox.addActionListener(e -> { - var box = (ComboBox) e.getSource(); - index = box.getSelectedIndex(); - }); - - var panel = UI.PanelFactory.grid() - .add(UI.PanelFactory.panel(comboBox) - .withLabel(DevPilotMessageBundle.get("devpilot.setting.language")) - .resizeX(false)) - .createPanel(); - panel.setBorder(JBUI.Borders.emptyLeft(0)); - return panel; - } - - private JPanel createOpenAIServiceSectionPanel(String label, JComponent component) { - var panel = UI.PanelFactory.grid() - .add(UI.PanelFactory.panel(component) - .withLabel(label) - .resizeX(false)) - .createPanel(); - panel.setBorder(JBUI.Borders.emptyLeft(16)); - return panel; - } - - private JPanel createOpenAIServicePanel() { - JPanel modelPanel = new JPanel(); - modelPanel.setLayout(new BoxLayout(modelPanel, BoxLayout.X_AXIS)); - modelPanel.add(openAIModelNameComboBox); - modelPanel.add(openAIRefreshModelBtn); - - var panel = UI.PanelFactory.grid() - .add(UI.PanelFactory.panel(openAIBaseHostField) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.modelHostLabel")) - .resizeX(false)) - .add(UI.PanelFactory.panel(openAIKeyField) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.apiKeyLabel")) - .resizeX(false)) - .add(UI.PanelFactory.panel(modelPanel) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.modelNameLabel")) - .resizeX(false)) - .add(UI.PanelFactory.panel(openAICustomModelNameField) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.customModelNameLabel")) - .resizeX(false)) - .createPanel(); - panel.setBorder(JBUI.Borders.emptyLeft(16)); - return panel; - } - - private JPanel createAIGatewayServicePanel() { - var panel = UI.PanelFactory.grid() - .add(UI.PanelFactory.panel(aiGatewayBaseHostField) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.modelHostLabel")) - .resizeX(false)) - .add(UI.PanelFactory.panel(aiGatewayModelComboBox) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.modelTypeLabel")) - .resizeX(false)) - .createPanel(); - panel.setBorder(JBUI.Borders.emptyLeft(16)); - return panel; - } - - private JPanel createCodeLlamaServicePanel() { - var panel = UI.PanelFactory.grid() - .add(UI.PanelFactory.panel(codeLlamaBaseHostField) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.modelHostLabel")) - .resizeX(false)) - .add(UI.PanelFactory.panel(codeLlamaModelNameField) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.modelNameLabel")) - .resizeX(false)) - .createPanel(); - panel.setBorder(JBUI.Borders.emptyLeft(16)); - return panel; - } - - private JPanel createOllamaServicePanel() { - JPanel modelPanel = new JPanel(); - modelPanel.setLayout(new BoxLayout(modelPanel, BoxLayout.X_AXIS)); - modelPanel.add(ollamaModelComboBox); - modelPanel.add(ollamaRefreshModelBtn); - - var panel = UI.PanelFactory.grid() - .add(UI.PanelFactory.panel(ollamaBaseHostField) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.modelHostLabel")) - .resizeX(false)) - .add(UI.PanelFactory.panel(modelPanel) - .withLabel(DevPilotMessageBundle.get("devpilot.settings.service.modelNameLabel")) - .resizeX(false)) - .createPanel(); - panel.setBorder(JBUI.Borders.emptyLeft(16)); - return panel; - } - - private void panelShow(ModelServiceEnum serviceEnum) { - switch (serviceEnum) { - case OPENAI: - openAIServicePanel.setVisible(true); - codeLlamaServicePanel.setVisible(false); - aiGatewayServicePanel.setVisible(false); - ollamaServicePanel.setVisible(false); - break; - case LLAMA: - openAIServicePanel.setVisible(false); - codeLlamaServicePanel.setVisible(true); - aiGatewayServicePanel.setVisible(false); - ollamaServicePanel.setVisible(false); - break; - case AIGATEWAY: - openAIServicePanel.setVisible(false); - codeLlamaServicePanel.setVisible(false); - aiGatewayServicePanel.setVisible(true); - ollamaServicePanel.setVisible(false); - break; - case OLLAMA: - openAIServicePanel.setVisible(false); - codeLlamaServicePanel.setVisible(false); - aiGatewayServicePanel.setVisible(false); - ollamaServicePanel.setVisible(true); - break; - default: - openAIServicePanel.setVisible(false); - codeLlamaServicePanel.setVisible(false); - aiGatewayServicePanel.setVisible(false); - ollamaServicePanel.setVisible(false); - break; - } - } - - public String getOpenAIBaseHost() { - return openAIBaseHostField.getText(); - } - - public String getOpenAIKey() { - return openAIKeyField.getText(); - } - - public String getCodeLlamaBaseHost() { - return codeLlamaBaseHostField.getText(); - } - - public ModelServiceEnum getSelectedModel() { - return (ModelServiceEnum) modelComboBox.getSelectedItem(); - } - - public Integer getLanguageIndex() { - return index; - } - - public String getAIGatewayBaseHost() { - return aiGatewayBaseHostField.getText(); - } - - public ModelTypeEnum getAIGatewayModel() { - return (ModelTypeEnum) aiGatewayModelComboBox.getSelectedItem(); - } - - public String getOpenAIModelName() { - String modelName = (String) openAIModelNameComboBox.getSelectedItem(); - return modelName == null ? "" : modelName; - } - - public String getOllamaBaseHost() { - return ollamaBaseHostField.getText(); - } - - public String getOllamaModelName() { - String modelName = (String) ollamaModelComboBox.getSelectedItem(); - return modelName == null ? "" : modelName; - } - - public String getOpenAICustomModelName() { - return openAICustomModelNameField.getText(); - } - - public String getCodeLlamaModelName() { - return codeLlamaModelNameField.getText(); - } -} diff --git a/src/main/java/com/zhongan/devpilot/settings/DevPilotSettingsComponent.java b/src/main/java/com/zhongan/devpilot/settings/DevPilotSettingsComponent.java index 66506211..893a9a9b 100644 --- a/src/main/java/com/zhongan/devpilot/settings/DevPilotSettingsComponent.java +++ b/src/main/java/com/zhongan/devpilot/settings/DevPilotSettingsComponent.java @@ -1,9 +1,13 @@ package com.zhongan.devpilot.settings; +import com.intellij.openapi.ui.ComboBox; import com.intellij.ui.TitledSeparator; +import com.intellij.ui.components.JBRadioButton; import com.intellij.ui.components.JBTextField; import com.intellij.util.ui.FormBuilder; +import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UI; +import com.zhongan.devpilot.settings.state.CompletionSettingsState; import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; import com.zhongan.devpilot.settings.state.LanguageSettingsState; import com.zhongan.devpilot.util.DevPilotMessageBundle; @@ -16,34 +20,58 @@ public class DevPilotSettingsComponent { private final JBTextField fullNameField; - private final DevPilotConfigForm devPilotConfigForm; + private final JBRadioButton autoCompletionRadio; - public DevPilotSettingsComponent(DevPilotSettingsConfigurable devPilotSettingsConfigurable, DevPilotLlmSettingsState settings) { - devPilotConfigForm = new DevPilotConfigForm(); + private Integer index; + public DevPilotSettingsComponent(DevPilotSettingsConfigurable devPilotSettingsConfigurable, DevPilotLlmSettingsState settings) { fullNameField = new JBTextField(settings.getFullName(), 20); + var instance = LanguageSettingsState.getInstance(); + index = instance.getLanguageIndex(); + Integer languageIndex = LanguageSettingsState.getInstance().getLanguageIndex(); + autoCompletionRadio = new JBRadioButton( + DevPilotMessageBundle.get("devpilot.settings.service.code.completion.desc"), + CompletionSettingsState.getInstance().getEnable()); + mainPanel = FormBuilder.createFormBuilder() .addComponent(UI.PanelFactory.panel(fullNameField) .withLabel(DevPilotMessageBundle.get("devpilot.setting.displayNameFieldLabel")) .resizeX(false) .createPanel()) - .addComponent(devPilotConfigForm.createLanguageSectionPanel(languageIndex)) - .addComponent(new TitledSeparator(DevPilotMessageBundle.get("devpilot.settings.service.title"))) - .addComponent(devPilotConfigForm.getForm()) + .addComponent(createLanguageSectionPanel(languageIndex)) + .addComponent(new TitledSeparator( + DevPilotMessageBundle.get("devpilot.settings.service.code.completion.title"))) + .addComponent(autoCompletionRadio) .addVerticalGap(8) .addComponentFillVertically(new JPanel(), 0) .getPanel(); } - public JPanel getPanel() { - return mainPanel; + public JPanel createLanguageSectionPanel(Integer languageIndex) { + var comboBox = new ComboBox<>(); + comboBox.addItem("English"); + comboBox.addItem("中文"); + comboBox.setSelectedIndex(languageIndex); + + comboBox.addActionListener(e -> { + var box = (ComboBox) e.getSource(); + index = box.getSelectedIndex(); + }); + + var panel = UI.PanelFactory.grid() + .add(UI.PanelFactory.panel(comboBox) + .withLabel(DevPilotMessageBundle.get("devpilot.setting.language")) + .resizeX(false)) + .createPanel(); + panel.setBorder(JBUI.Borders.emptyLeft(0)); + return panel; } - public DevPilotConfigForm getDevPilotConfigForm() { - return devPilotConfigForm; + public JPanel getPanel() { + return mainPanel; } // Getting the full name from the settings @@ -52,6 +80,10 @@ public String getFullName() { } public Integer getLanguageIndex() { - return devPilotConfigForm.getLanguageIndex(); + return index; + } + + public boolean getCompletionEnabled() { + return autoCompletionRadio.isSelected(); } } diff --git a/src/main/java/com/zhongan/devpilot/settings/DevPilotSettingsConfigurable.java b/src/main/java/com/zhongan/devpilot/settings/DevPilotSettingsConfigurable.java index 0e59b313..e479b18d 100644 --- a/src/main/java/com/zhongan/devpilot/settings/DevPilotSettingsConfigurable.java +++ b/src/main/java/com/zhongan/devpilot/settings/DevPilotSettingsConfigurable.java @@ -5,13 +5,12 @@ import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.util.NlsContexts; import com.zhongan.devpilot.actions.editor.popupmenu.PopupMenuEditorActionGroupUtil; -import com.zhongan.devpilot.settings.state.AIGatewaySettingsState; -import com.zhongan.devpilot.settings.state.CodeLlamaSettingsState; +import com.zhongan.devpilot.enums.LoginTypeEnum; +import com.zhongan.devpilot.settings.state.CompletionSettingsState; import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; import com.zhongan.devpilot.settings.state.LanguageSettingsState; -import com.zhongan.devpilot.settings.state.OllamaSettingsState; -import com.zhongan.devpilot.settings.state.OpenAISettingsState; import com.zhongan.devpilot.util.ConfigChangeUtils; +import com.zhongan.devpilot.util.ConfigurableUtils; import com.zhongan.devpilot.util.DevPilotMessageBundle; import javax.swing.JComponent; @@ -33,34 +32,20 @@ public class DevPilotSettingsConfigurable implements Configurable, Disposable { public @Nullable JComponent createComponent() { var settings = DevPilotLlmSettingsState.getInstance(); settingsComponent = new DevPilotSettingsComponent(this, settings); + ConfigurableUtils.setConfigurableCache(settingsComponent); return settingsComponent.getPanel(); } @Override public boolean isModified() { var settings = DevPilotLlmSettingsState.getInstance(); - var openAISettings = OpenAISettingsState.getInstance(); - var aiGatewaySettings = AIGatewaySettingsState.getInstance(); var languageSettings = LanguageSettingsState.getInstance(); - var codeLlamaSettings = CodeLlamaSettingsState.getInstance(); - var ollamaSettings = OllamaSettingsState.getInstance(); - var serviceForm = settingsComponent.getDevPilotConfigForm(); - var selectedModel = serviceForm.getSelectedModel(); - var selectedModelType = serviceForm.getAIGatewayModel(); + var languageIndex = settingsComponent.getLanguageIndex(); + var completionEnable = CompletionSettingsState.getInstance().getEnable(); return !settingsComponent.getFullName().equals(settings.getFullName()) - || !selectedModel.getName().equals(settings.getSelectedModel()) - || !selectedModelType.getName().equals(aiGatewaySettings.getSelectedModel()) - || !serviceForm.getOpenAIBaseHost().equals(openAISettings.getModelHost()) - || !serviceForm.getOpenAIModelName().equals(openAISettings.getModelName()) - || !serviceForm.getOpenAICustomModelName().equals(openAISettings.getCustomModelName()) - || !serviceForm.getAIGatewayBaseHost().equals(aiGatewaySettings.getModelBaseHost(selectedModelType.getName())) - || !serviceForm.getOpenAIKey().equals(openAISettings.getPrivateKey()) - || !serviceForm.getCodeLlamaBaseHost().equals(codeLlamaSettings.getModelHost()) - || !serviceForm.getCodeLlamaModelName().equals(codeLlamaSettings.getModelName()) - || !serviceForm.getOllamaModelName().equals(ollamaSettings.getModelName()) - || !serviceForm.getOllamaBaseHost().equals(ollamaSettings.getModelHost()) - || !serviceForm.getLanguageIndex().equals(languageSettings.getLanguageIndex()); + || !languageIndex.equals(languageSettings.getLanguageIndex()) + || !settingsComponent.getCompletionEnabled() == (completionEnable); } @Override @@ -80,32 +65,21 @@ public void apply() throws ConfigurationException { PopupMenuEditorActionGroupUtil.refreshActions(null); - var openAISettings = OpenAISettingsState.getInstance(); - var aiGatewaySettings = AIGatewaySettingsState.getInstance(); - var codeLlamaSettings = CodeLlamaSettingsState.getInstance(); - var ollamaSettings = OllamaSettingsState.getInstance(); - var serviceForm = settingsComponent.getDevPilotConfigForm(); - var selectedModel = serviceForm.getSelectedModel(); - var selectedModelType = serviceForm.getAIGatewayModel(); - var openAIModelName = serviceForm.getOpenAIModelName(); - String ollamaBaseHost = serviceForm.getOllamaBaseHost(); - String ollamaModelName = serviceForm.getOllamaModelName(); - - settings.setSelectedModel(selectedModel.getName()); - openAISettings.setModelHost(serviceForm.getOpenAIBaseHost()); - openAISettings.setPrivateKey(serviceForm.getOpenAIKey()); - openAISettings.setModelName(openAIModelName); - openAISettings.setCustomModelName(serviceForm.getOpenAICustomModelName()); - codeLlamaSettings.setModelHost(serviceForm.getCodeLlamaBaseHost()); - codeLlamaSettings.setModelName(serviceForm.getCodeLlamaModelName()); - aiGatewaySettings.setModelBaseHost(selectedModelType.getName(), serviceForm.getAIGatewayBaseHost()); - aiGatewaySettings.setSelectedModel(selectedModelType.getName()); - ollamaSettings.setModelHost(ollamaBaseHost); - ollamaSettings.setModelName(ollamaModelName); + CompletionSettingsState completionSettings = CompletionSettingsState.getInstance(); + completionSettings.setEnable(settingsComponent.getCompletionEnabled()); + + checkCodeCompletionConfig(LoginTypeEnum.getLoginTypeEnum(settings.getLoginType())); } @Override public void dispose() { } + private void checkCodeCompletionConfig(LoginTypeEnum loginType) { + if (!(LoginTypeEnum.ZA.equals(loginType) || LoginTypeEnum.ZA_TI.equals(loginType)) + && CompletionSettingsState.getInstance().getEnable()) { + CompletionSettingsState.getInstance().setEnable(false); + } + } + } diff --git a/src/main/java/com/zhongan/devpilot/settings/actionconfiguration/EditorActionConfigurationState.java b/src/main/java/com/zhongan/devpilot/settings/actionconfiguration/EditorActionConfigurationState.java index 86b75162..dd34ddd7 100644 --- a/src/main/java/com/zhongan/devpilot/settings/actionconfiguration/EditorActionConfigurationState.java +++ b/src/main/java/com/zhongan/devpilot/settings/actionconfiguration/EditorActionConfigurationState.java @@ -22,14 +22,17 @@ ) public class EditorActionConfigurationState implements PersistentStateComponent { - private final Map defaultActions = new LinkedHashMap<>(Map.of( - PERFORMANCE_CHECK.getLabel(), PERFORMANCE_CHECK.getPrompt(), - GENERATE_COMMENTS.getLabel(), GENERATE_COMMENTS.getPrompt(), - GENERATE_TESTS.getLabel(), GENERATE_TESTS.getPrompt(), - FIX_THIS.getLabel(), FIX_THIS.getPrompt(), - EXPLAIN_THIS.getLabel(), EXPLAIN_THIS.getPrompt(), - REVIEW_CODE.getLabel(), REVIEW_CODE.getPrompt() - )); + private final Map defaultActions; + + { + defaultActions = new LinkedHashMap<>(); + defaultActions.put(EXPLAIN_THIS.getLabel(), EXPLAIN_THIS.getPrompt()); + defaultActions.put(FIX_THIS.getLabel(), FIX_THIS.getPrompt()); + defaultActions.put(PERFORMANCE_CHECK.getLabel(), PERFORMANCE_CHECK.getPrompt()); + defaultActions.put(GENERATE_COMMENTS.getLabel(), GENERATE_COMMENTS.getPrompt()); + defaultActions.put(GENERATE_TESTS.getLabel(), GENERATE_TESTS.getPrompt()); + defaultActions.put(REVIEW_CODE.getLabel(), REVIEW_CODE.getPrompt()); + } public static EditorActionConfigurationState getInstance() { return ApplicationManager.getApplication().getService(EditorActionConfigurationState.class); diff --git a/src/main/java/com/zhongan/devpilot/settings/state/AIGatewaySettingsState.java b/src/main/java/com/zhongan/devpilot/settings/state/AIGatewaySettingsState.java index 86c034a9..57cf1636 100644 --- a/src/main/java/com/zhongan/devpilot/settings/state/AIGatewaySettingsState.java +++ b/src/main/java/com/zhongan/devpilot/settings/state/AIGatewaySettingsState.java @@ -6,16 +6,32 @@ import com.intellij.openapi.components.Storage; import com.intellij.util.xmlb.XmlSerializerUtil; import com.zhongan.devpilot.enums.ModelTypeEnum; +import com.zhongan.devpilot.enums.ZaSsoEnum; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import static com.zhongan.devpilot.constant.DefaultConst.AI_GATEWAY_DEFAULT_HOST; + @State(name = "DevPilot_AIGatewaySettings", storages = @Storage("DevPilot_AIGatewaySettings.xml")) public class AIGatewaySettingsState implements PersistentStateComponent { private String selectedModel = ModelTypeEnum.GPT3_5.getName(); private Map modelBaseHostMap = new ConcurrentHashMap<>(); + @Deprecated + private String selectedSso = ZaSsoEnum.ZA.getName(); + + // za + private String ssoToken; + + private String ssoUsername; + + // za_ti + private String tiSsoToken; + + private String tiSsoUsername; + public static AIGatewaySettingsState getInstance() { return ApplicationManager.getApplication().getService(AIGatewaySettingsState.class); } @@ -29,8 +45,7 @@ public void setSelectedModel(String selectedModel) { } public String getModelBaseHost(String selectedModel) { - String openAIBaseHost = ""; - return modelBaseHostMap.getOrDefault(selectedModel, openAIBaseHost); + return AI_GATEWAY_DEFAULT_HOST; } public void setModelBaseHost(String model, String host) { @@ -45,6 +60,46 @@ public void setModelBaseHostMap(Map modelBaseHostMap) { this.modelBaseHostMap = modelBaseHostMap; } + public String getSelectedSso() { + return selectedSso; + } + + public void setSelectedSso(String selectedSso) { + this.selectedSso = selectedSso; + } + + public String getSsoToken() { + return ssoToken; + } + + public void setSsoToken(String ssoToken) { + this.ssoToken = ssoToken; + } + + public String getSsoUsername() { + return ssoUsername; + } + + public void setSsoUsername(String ssoUsername) { + this.ssoUsername = ssoUsername; + } + + public String getTiSsoToken() { + return tiSsoToken; + } + + public void setTiSsoToken(String tiSsoToken) { + this.tiSsoToken = tiSsoToken; + } + + public String getTiSsoUsername() { + return tiSsoUsername; + } + + public void setTiSsoUsername(String tiSsoUsername) { + this.tiSsoUsername = tiSsoUsername; + } + @Override public AIGatewaySettingsState getState() { return this; diff --git a/src/main/java/com/zhongan/devpilot/settings/state/ChatShortcutSettingState.java b/src/main/java/com/zhongan/devpilot/settings/state/ChatShortcutSettingState.java new file mode 100644 index 00000000..6581a618 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/settings/state/ChatShortcutSettingState.java @@ -0,0 +1,38 @@ +package com.zhongan.devpilot.settings.state; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.util.xmlb.XmlSerializerUtil; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@State(name = "DevPilot_ChatShortcutSetting", storages = @Storage("DevPilot_ChatShortcutSetting.xml")) +public class ChatShortcutSettingState implements PersistentStateComponent { + + private Boolean enable = true; + + public static ChatShortcutSettingState getInstance() { + return ApplicationManager.getApplication().getService(ChatShortcutSettingState.class); + } + + public Boolean getEnable() { + return enable; + } + + public void setEnable(Boolean enable) { + this.enable = enable; + } + + @Override + public @Nullable ChatShortcutSettingState getState() { + return this; + } + + @Override + public void loadState(@NotNull ChatShortcutSettingState state) { + XmlSerializerUtil.copyBean(state, this); + } +} diff --git a/src/main/java/com/zhongan/devpilot/settings/state/CompletionSettingsState.java b/src/main/java/com/zhongan/devpilot/settings/state/CompletionSettingsState.java new file mode 100644 index 00000000..ee4f598d --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/settings/state/CompletionSettingsState.java @@ -0,0 +1,37 @@ +package com.zhongan.devpilot.settings.state; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.util.xmlb.XmlSerializerUtil; + +@State(name = "DevPilot_CompletionSettings", storages = @Storage("DevPilot_CompletionSettings.xml")) +public class CompletionSettingsState implements PersistentStateComponent { + + private Boolean enable = true; + + public static CompletionSettingsState getInstance() { + return ApplicationManager.getApplication().getService(CompletionSettingsState.class); + } + + public Boolean getEnable() { + return enable == null ? true : enable; + } + + public void setEnable(Boolean enable) { + this.enable = enable; + } + + @Override + public CompletionSettingsState getState() { + return this; + } + + @Override + public void loadState(CompletionSettingsState state) { + XmlSerializerUtil.copyBean(state, this); + } + +} + diff --git a/src/main/java/com/zhongan/devpilot/settings/state/DevPilotLlmSettingsState.java b/src/main/java/com/zhongan/devpilot/settings/state/DevPilotLlmSettingsState.java index ada9d7cd..907db513 100644 --- a/src/main/java/com/zhongan/devpilot/settings/state/DevPilotLlmSettingsState.java +++ b/src/main/java/com/zhongan/devpilot/settings/state/DevPilotLlmSettingsState.java @@ -5,6 +5,7 @@ import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.util.xmlb.XmlSerializerUtil; +import com.zhongan.devpilot.enums.LoginTypeEnum; import com.zhongan.devpilot.enums.ModelServiceEnum; import java.util.UUID; @@ -16,7 +17,10 @@ public class DevPilotLlmSettingsState implements PersistentStateComponent { - private String modelHost = "http://localhost:11434"; - - private String modelName = ""; - - public static OllamaSettingsState getInstance() { - return ApplicationManager.getApplication().getService(OllamaSettingsState.class); - } - - public String getModelHost() { - return modelHost; - } - - public void setModelHost(String modelHost) { - this.modelHost = modelHost; - } - - public String getModelName() { - return modelName; - } - - public void setModelName(String modelName) { - this.modelName = modelName; - } - - @Override - public OllamaSettingsState getState() { - return this; - } - - @Override - public void loadState(OllamaSettingsState state) { - XmlSerializerUtil.copyBean(state, this); - } - -} diff --git a/src/main/java/com/zhongan/devpilot/settings/state/OpenAISettingsState.java b/src/main/java/com/zhongan/devpilot/settings/state/OpenAISettingsState.java index 60a5cb1e..ef2bc0cb 100644 --- a/src/main/java/com/zhongan/devpilot/settings/state/OpenAISettingsState.java +++ b/src/main/java/com/zhongan/devpilot/settings/state/OpenAISettingsState.java @@ -5,16 +5,17 @@ import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.util.xmlb.XmlSerializerUtil; +import com.zhongan.devpilot.enums.OpenAIModelNameEnum; @State(name = "DevPilot_OpenAISettings", storages = @Storage("DevPilot_OpenAISettings.xml")) public class OpenAISettingsState implements PersistentStateComponent { - private String modelHost = ""; + private String modelHost; - private String privateKey = ""; + private String privateKey; - private String modelName = ""; + private String modelName = OpenAIModelNameEnum.GPT3_5_TURBO.getName(); - private String customModelName = ""; + private String customModelName; public static OpenAISettingsState getInstance() { return ApplicationManager.getApplication().getService(OpenAISettingsState.class); diff --git a/src/main/java/com/zhongan/devpilot/settings/state/TrialServiceSettingsState.java b/src/main/java/com/zhongan/devpilot/settings/state/TrialServiceSettingsState.java new file mode 100644 index 00000000..53c86774 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/settings/state/TrialServiceSettingsState.java @@ -0,0 +1,54 @@ +package com.zhongan.devpilot.settings.state; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.util.xmlb.XmlSerializerUtil; + +@State(name = "DevPilot_TrialServiceSettings", storages = @Storage("DevPilot_TrialServiceSettings.xml")) +public class TrialServiceSettingsState implements PersistentStateComponent { + private String wxToken; + + private String wxUsername; + + private String wxUserId; + + public static TrialServiceSettingsState getInstance() { + return ApplicationManager.getApplication().getService(TrialServiceSettingsState.class); + } + + public String getWxToken() { + return wxToken; + } + + public void setWxToken(String wxToken) { + this.wxToken = wxToken; + } + + public String getWxUsername() { + return wxUsername; + } + + public void setWxUsername(String wxUsername) { + this.wxUsername = wxUsername; + } + + public String getWxUserId() { + return wxUserId; + } + + public void setWxUserId(String wxUserId) { + this.wxUserId = wxUserId; + } + + @Override + public TrialServiceSettingsState getState() { + return this; + } + + @Override + public void loadState(TrialServiceSettingsState state) { + XmlSerializerUtil.copyBean(state, this); + } +} diff --git a/src/main/java/com/zhongan/devpilot/statusBar/DevPilotStatusBarBaseWidget.java b/src/main/java/com/zhongan/devpilot/statusBar/DevPilotStatusBarBaseWidget.java new file mode 100644 index 00000000..1a9e8c06 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/statusBar/DevPilotStatusBarBaseWidget.java @@ -0,0 +1,72 @@ +package com.zhongan.devpilot.statusBar; + +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.openapi.ui.popup.ListPopup; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.StatusBar; +import com.intellij.openapi.wm.StatusBarWidget; +import com.intellij.openapi.wm.WindowManager; +import com.intellij.openapi.wm.impl.status.EditorBasedStatusBarPopup; +import com.zhongan.devpilot.statusBar.status.DevPilotStatusEnum; +import com.zhongan.devpilot.util.LoginUtils; + +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class DevPilotStatusBarBaseWidget extends EditorBasedStatusBarPopup { + + private static DevPilotStatusEnum currentStatus = LoginUtils.isLogin() ? DevPilotStatusEnum.LoggedIn : DevPilotStatusEnum.NotLoggedIn; + + public DevPilotStatusBarBaseWidget(@NotNull Project project) { + super(project, false); + } + + @Override + protected @NotNull WidgetState getWidgetState(@Nullable VirtualFile file) { + WidgetState widgetState = new WidgetState(currentStatus.getText(), "", true); + widgetState.setIcon(currentStatus.getIcon()); + return widgetState; + } + + @Override + protected @Nullable ListPopup createPopup(DataContext context) { + return JBPopupFactory. + getInstance(). + createActionGroupPopup("DevPilot Status", + StatusBarActions.buildStatusBarActionsGroup(), + context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true); + } + + @Override + protected @NotNull StatusBarWidget createInstance(@NotNull Project project) { + return new DevPilotStatusBarBaseWidget(project); + } + + @Override + public @NonNls @NotNull String ID() { + return "com.zhongan.devpilot.status.widget"; + } + + public static void update(Project project, DevPilotStatusEnum devPilotStatus) { + currentStatus = devPilotStatus; + DevPilotStatusBarBaseWidget statusBarWidget = findStatusBarWidget(project); + if (statusBarWidget != null) { + statusBarWidget.update(() -> statusBarWidget.myStatusBar.updateWidget("com.zhongan.devpilot.status.widget")); + } + } + + private static DevPilotStatusBarBaseWidget findStatusBarWidget(@NotNull Project project) { + StatusBar statusBar = WindowManager.getInstance().getStatusBar(project); + if (statusBar != null) { + StatusBarWidget widget = statusBar.getWidget("com.zhongan.devpilot.status.widget"); + if (widget instanceof DevPilotStatusBarBaseWidget) { + return (DevPilotStatusBarBaseWidget) widget; + } + } + + return null; + } +} diff --git a/src/main/java/com/zhongan/devpilot/statusBar/DevPilotStatusBarWidgetFactory.java b/src/main/java/com/zhongan/devpilot/statusBar/DevPilotStatusBarWidgetFactory.java new file mode 100644 index 00000000..9e69fbcb --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/statusBar/DevPilotStatusBarWidgetFactory.java @@ -0,0 +1,35 @@ +package com.zhongan.devpilot.statusBar; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.StatusBarWidget; +import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory; + +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +public class DevPilotStatusBarWidgetFactory extends StatusBarEditorBasedWidgetFactory { + public DevPilotStatusBarWidgetFactory() { + + } + + @Override + public @NonNls @NotNull String getId() { + return "com.zhongan.devpilot.status.widget"; + } + + @Override + public @Nls @NotNull String getDisplayName() { + return "DevPilot"; + } + + @Override + public @NotNull StatusBarWidget createWidget(@NotNull Project project) { + return new DevPilotStatusBarBaseWidget(project); + } + + @Override + public void disposeWidget(@NotNull StatusBarWidget widget) { + + } +} diff --git a/src/main/java/com/zhongan/devpilot/statusBar/StatusBarActions.java b/src/main/java/com/zhongan/devpilot/statusBar/StatusBarActions.java new file mode 100644 index 00000000..267f0c70 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/statusBar/StatusBarActions.java @@ -0,0 +1,114 @@ +package com.zhongan.devpilot.statusBar; + +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DefaultActionGroup; +import com.intellij.openapi.options.ShowSettingsUtil; +import com.intellij.openapi.project.DumbAwareAction; +import com.intellij.util.Consumer; +import com.zhongan.devpilot.DevPilotIcons; +import com.zhongan.devpilot.settings.DevPilotSettingsConfigurable; +import com.zhongan.devpilot.settings.state.ChatShortcutSettingState; +import com.zhongan.devpilot.settings.state.CompletionSettingsState; +import com.zhongan.devpilot.util.DevPilotMessageBundle; +import com.zhongan.devpilot.util.LoginUtils; +import com.zhongan.devpilot.util.ThemeUtils; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.Icon; + +import org.jetbrains.annotations.NotNull; + +public class StatusBarActions { + + public static DefaultActionGroup buildStatusBarActionsGroup() { + List actions = new ArrayList<>(); + if (LoginUtils.isLogin()) { + actions.add(createAccountShowAction()); + } + actions.add(createLoginAction()); + actions.add(createEditSettingsAction()); + actions.add(createChatShortcutSwitchAction()); + actions.add(createCompletionsAction()); + return new DefaultActionGroup(actions); + } + + private static DumbAwareAction createCompletionsAction() { + if (CompletionSettingsState.getInstance().getEnable()) { + return DumbAwareAction.create( + DevPilotMessageBundle.get("devpilot.settings.service.code.completion.disabled.desc"), + event -> CompletionSettingsState.getInstance().setEnable(false) + ); + } else { + return DumbAwareAction.create( + DevPilotMessageBundle.get("devpilot.settings.service.code.completion.enable.desc"), + event -> CompletionSettingsState.getInstance().setEnable(true) + ); + } + } + + private static DumbAwareAction createLoginAction() { + if (LoginUtils.isLogin()) { + return createActionWithIcon( + DevPilotMessageBundle.get("devpilot.settings.service.statusbar.logout.desc"), + ThemeUtils.isDarkTheme() ? DevPilotIcons.LOGOUT_DARK : DevPilotIcons.LOGOUT, + event -> LoginUtils.logout(), true + ); + } else { + return createActionWithIcon( + DevPilotMessageBundle.get("devpilot.settings.service.statusbar.login.desc"), + ThemeUtils.isDarkTheme() ? DevPilotIcons.LOGIN_DARK : DevPilotIcons.LOGIN, + event -> LoginUtils.gotoLogin(), true); + } + } + + private static DumbAwareAction createChatShortcutSwitchAction() { + if (ChatShortcutSettingState.getInstance().getEnable()) { + return DumbAwareAction.create( + DevPilotMessageBundle.get("devpilot.settings.service.chat.shortcut.disabled.desc"), + event -> ChatShortcutSettingState.getInstance().setEnable(false) + ); + } else { + return DumbAwareAction.create( + DevPilotMessageBundle.get("devpilot.settings.service.chat.shortcut.enable.desc"), + event -> ChatShortcutSettingState.getInstance().setEnable(true) + ); + } + } + + private static DumbAwareAction createEditSettingsAction() { + return createActionWithIcon( + DevPilotMessageBundle.get("devpilot.action.edit.settings"), + ThemeUtils.isDarkTheme() ? DevPilotIcons.SETTINGS_DARK : DevPilotIcons.SETTINGS, + event -> ShowSettingsUtil.getInstance().showSettingsDialog(event.getProject(), DevPilotSettingsConfigurable.class), + true + ); + } + + private static DumbAwareAction createAccountShowAction() { + String account = DevPilotMessageBundle.get("devpilot.status.account"); + String userName = LoginUtils.getUsername(); + return createActionWithIcon( + account + userName, + ThemeUtils.isDarkTheme() ? DevPilotIcons.ACCOUNT_DARK : DevPilotIcons.ACCOUNT, + null, false); + } + + private static DumbAwareAction createActionWithIcon(String text, Icon icon, + Consumer actionPerformed, Boolean enabled) { + return new DumbAwareAction(text, "", icon) { + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + actionPerformed.consume(e); + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(enabled); + } + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/statusBar/status/DevPilotStatusEnum.java b/src/main/java/com/zhongan/devpilot/statusBar/status/DevPilotStatusEnum.java new file mode 100644 index 00000000..e4d4cb4b --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/statusBar/status/DevPilotStatusEnum.java @@ -0,0 +1,37 @@ +package com.zhongan.devpilot.statusBar.status; + +import com.zhongan.devpilot.DevPilotIcons; +import com.zhongan.devpilot.util.DevPilotMessageBundle; +import com.zhongan.devpilot.util.ThemeUtils; + +import javax.swing.Icon; + +import org.jetbrains.annotations.NotNull; + +public enum DevPilotStatusEnum { + LoggedIn, + NotLoggedIn, + InCompletion; + + public @NotNull Icon getIcon() { + switch (this) { + case LoggedIn: + return DevPilotIcons.SYSTEM_ICON_13; + case InCompletion: + return DevPilotIcons.COMPLETION_IN_PROGRESS; + default: + return ThemeUtils.isDarkTheme() ? DevPilotIcons.DISCONNECT_DARK : DevPilotIcons.DISCONNECT; + } + } + + public String getText() { + switch (this) { + case LoggedIn: + return DevPilotMessageBundle.get("devpilot.status.loggedIn"); + case InCompletion: + return DevPilotMessageBundle.get("devpilot.status.inCompletion"); + default: + return DevPilotMessageBundle.get("devpilot.status.notLoggedIn"); + } + } +} diff --git a/src/main/java/com/zhongan/devpilot/update/DevPilotUpdate.java b/src/main/java/com/zhongan/devpilot/update/DevPilotUpdate.java new file mode 100644 index 00000000..b409150e --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/update/DevPilotUpdate.java @@ -0,0 +1,66 @@ +package com.zhongan.devpilot.update; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.extensions.PluginId; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.updateSettings.impl.PluginDownloader; +import com.intellij.openapi.updateSettings.impl.UpdateChecker; +import com.intellij.openapi.updateSettings.impl.UpdateSettings; +import com.intellij.openapi.util.BuildNumber; +import com.zhongan.devpilot.actions.notifications.DevPilotNotification; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; + +import org.jetbrains.annotations.NotNull; + +public class DevPilotUpdate { + + public static void installUpdate(Project project) { + UpdateSettings settingsCopy = new UpdateSettings(); + settingsCopy.getState().copyFrom(UpdateSettings.getInstance().getState()); + settingsCopy.getState().setCheckNeeded(true); + settingsCopy.getState().setPluginsCheckNeeded(true); + settingsCopy.getState().setThirdPartyPluginsAllowed(true); + settingsCopy.getState().setShowWhatsNewEditor(false); + UpdateChecker.updateAndShowResult(project, settingsCopy); + } + + public static final class DevPilotUpdateTask extends Task.Backgroundable { + public DevPilotUpdateTask(Project project) { + super(project, "Check DevPilot Update", false); + } + + @Override + public void run(@NotNull ProgressIndicator indicator) { + try { + checkUpdate(getProject(), indicator); + } catch (Exception e) { + Logger.getInstance(getClass()).warn("Check update failed: " + e.getMessage()); + } + } + } + + private static void checkUpdate(Project project, ProgressIndicator indicator) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + var pluginId = PluginId.getId("com.zhongan.devPilot"); + + var getInternalPluginUpdatesMethod = + UpdateChecker.class.getMethod( + "getInternalPluginUpdates", BuildNumber.class, ProgressIndicator.class); + var internalPluginUpdates = getInternalPluginUpdatesMethod.invoke(null, null, indicator); + var getPluginUpdatesMethod = internalPluginUpdates.getClass().getMethod("getPluginUpdates"); + var pluginUpdates = getPluginUpdatesMethod.invoke(internalPluginUpdates); + var getAllEnabledMethod = pluginUpdates.getClass().getMethod("getAllEnabled"); + var allEnabled = getAllEnabledMethod.invoke(pluginUpdates); + + var list = (Collection) allEnabled; + + boolean shouldUpdate = list.stream().anyMatch((item) -> pluginId.equals(item.getId())); + + if (shouldUpdate) { + DevPilotNotification.updateNotification(project); + } + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/CallbackUtils.java b/src/main/java/com/zhongan/devpilot/util/CallbackUtils.java new file mode 100644 index 00000000..5a5bf489 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/CallbackUtils.java @@ -0,0 +1,36 @@ +package com.zhongan.devpilot.util; + +import com.zhongan.devpilot.rest.LoginAuthCallbackRestService; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +import java.io.IOException; +import java.io.InputStream; + +public class CallbackUtils { + public static ByteBuf getRedirectPage(String url) throws IOException { + InputStream inputStream = LoginAuthCallbackRestService.class.getResourceAsStream(url); + + if (inputStream == null) { + return Unpooled.EMPTY_BUFFER; + } + + return Unpooled.wrappedBuffer(inputStream.readAllBytes()); + } + + public static DefaultFullHttpResponse buildSuccessResponse() throws IOException { + var response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, getRedirectPage("/html/login-success.html")); + response.headers().set("Content-Type", "text/html"); + return response; + } + + public static DefaultFullHttpResponse buildFailResponse() throws IOException { + var response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, getRedirectPage("/html/login-fail.html")); + response.headers().set("Content-Type", "text/html"); + return response; + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/CommentUtil.java b/src/main/java/com/zhongan/devpilot/util/CommentUtil.java new file mode 100644 index 00000000..108411d9 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/CommentUtil.java @@ -0,0 +1,40 @@ + +package com.zhongan.devpilot.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CommentUtil { + + private static final String MULTI_LINE_REGEX = "/\\*(.|\\n)*?\\*/"; + + private static final String SINGLE_LINE_REGEX = "//.*"; + + private static final Pattern MULTI_LINE_PATTERN = Pattern.compile(MULTI_LINE_REGEX); + + private static final Pattern SINGLE_LINE_PATTERN = Pattern.compile(SINGLE_LINE_REGEX); + + public static boolean isMultiLineComment(String text) { + Matcher multiLineCommentMatcher = MULTI_LINE_PATTERN.matcher(text); + + while (multiLineCommentMatcher.find()) { + return true; + } + return false; + } + + public static boolean isSingleLineComment(String text) { + Matcher singleLineCommentMatcher = SINGLE_LINE_PATTERN.matcher(text); + + while (singleLineCommentMatcher.find()) { + return true; + } + return false; + } + + public static boolean containsComment(String text) { + if (text == null) return false; + return isMultiLineComment(text) || isSingleLineComment(text); + } + +} \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/util/ConfigBundleUtils.java b/src/main/java/com/zhongan/devpilot/util/ConfigBundleUtils.java new file mode 100644 index 00000000..fd52124f --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/ConfigBundleUtils.java @@ -0,0 +1,37 @@ +package com.zhongan.devpilot.util; + +import java.util.ResourceBundle; + +import org.apache.commons.lang3.StringUtils; + +public class ConfigBundleUtils { + private static final ResourceBundle bundle; + + static { + ResourceBundle tmp; + try { + tmp = ResourceBundle.getBundle("config.local"); + } catch (Exception e) { + tmp = null; + } + bundle = tmp; + } + + public static String getConfig(String key) { + return getConfig(key, null); + } + + public static String getConfig(String key, String defaultValue) { + if (bundle == null) { + return defaultValue; + } + + String value = bundle.getString(key); + + if (StringUtils.isEmpty(value)) { + return defaultValue; + } + + return value; + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/ConfigChangeUtils.java b/src/main/java/com/zhongan/devpilot/util/ConfigChangeUtils.java index ed386779..4b83a1c7 100644 --- a/src/main/java/com/zhongan/devpilot/util/ConfigChangeUtils.java +++ b/src/main/java/com/zhongan/devpilot/util/ConfigChangeUtils.java @@ -2,37 +2,36 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; +import com.zhongan.devpilot.DevPilotVersion; import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; import com.zhongan.devpilot.settings.state.LanguageSettingsState; import com.zhongan.devpilot.webview.model.ConfigModel; -import javax.swing.UIManager; - public class ConfigChangeUtils { public static ConfigModel configInit() { - var nowTheme = UIManager.getLookAndFeel().getName(); var language = LanguageSettingsState.getInstance().getLanguageIndex(); var locale = (language == 1) ? "cn" : "en"; - var theme = (nowTheme.contains("Light")) ? "light" : "dark"; var username = DevPilotLlmSettingsState.getInstance().getFullName(); + var loggedIn = LoginUtils.isLogin(); + var env = System.getProperty("devpilot.env") == null ? "prd" : System.getProperty("devpilot.env"); + var version = DevPilotVersion.getDevPilotVersion(); + var platform = DevPilotVersion.getVersionName(); - return new ConfigModel(theme, locale, username); + return new ConfigModel(ThemeUtils.themeType(), locale, username, loggedIn, env, version, platform); } public static void themeChanged(Project project) { - var theme = UIManager.getLookAndFeel().getName(); var service = project.getService(DevPilotChatToolWindowService.class); - service.changeTheme(theme); + service.changeTheme(ThemeUtils.themeType()); } public static void themeChanged() { - var theme = UIManager.getLookAndFeel().getName(); var projects = ProjectManager.getInstance().getOpenProjects(); for (var project : projects) { var service = project.getService(DevPilotChatToolWindowService.class); - service.changeTheme(theme); + service.changeTheme(ThemeUtils.themeType()); } } diff --git a/src/main/java/com/zhongan/devpilot/util/ConfigurableUtils.java b/src/main/java/com/zhongan/devpilot/util/ConfigurableUtils.java new file mode 100644 index 00000000..12c29e22 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/ConfigurableUtils.java @@ -0,0 +1,15 @@ +package com.zhongan.devpilot.util; + +import com.zhongan.devpilot.settings.DevPilotSettingsComponent; + +public class ConfigurableUtils { + private static DevPilotSettingsComponent component; + + public static void setConfigurableCache(DevPilotSettingsComponent component) { + ConfigurableUtils.component = component; + } + + public static DevPilotSettingsComponent getConfigurableCache() { + return ConfigurableUtils.component; + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/EditorUtils.java b/src/main/java/com/zhongan/devpilot/util/EditorUtils.java index 75ed6ee5..e2681751 100644 --- a/src/main/java/com/zhongan/devpilot/util/EditorUtils.java +++ b/src/main/java/com/zhongan/devpilot/util/EditorUtils.java @@ -9,16 +9,22 @@ import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.popup.Balloon; +import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.openapi.vfs.VirtualFileManager; + +import java.util.HashMap; +import java.util.Map; import org.jetbrains.annotations.NotNull; public class EditorUtils { + // repo和根目录的映射关系,用于打开文件时快速定位到根目录 + private static final Map repoMapping = new HashMap<>(); + public static void openFileAndSelectLines(@NotNull Project project, String fileUrl, int startLine, int endLine) { - VirtualFile codeFile = VirtualFileManager.getInstance().findFileByUrl(fileUrl); + VirtualFile codeFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(fileUrl); if (codeFile == null || !codeFile.exists()) { BalloonAlertUtils.showErrorAlert(DevPilotMessageBundle.get("devpilot.alter.file.not.exist"), 0, -10, Balloon.Position.above); return; @@ -35,4 +41,35 @@ public static void openFileAndSelectLines(@NotNull Project project, String fileU scrollingModel.scrollTo(new LogicalPosition(startLine, 0), ScrollType.CENTER); } } + + public static String getCurrentEditorRepositoryName(Project project) { + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + VirtualFile currentFile = fileEditorManager.getSelectedFiles()[0]; + String repoName = GitUtil.getRepoNameFromFile(project, currentFile); + String basePath = project.getBasePath(); + + if (basePath != null) { + VirtualFile baseDir = LocalFileSystem.getInstance().refreshAndFindFileByPath(basePath); + repoMapping.putIfAbsent(repoName, baseDir); + } + + return repoName; + } + + public static void openFileByRelativePath(String repo, @NotNull Project project, String relativePath) { + VirtualFile baseDir = repoMapping.get(repo); + + if (baseDir == null) { + BalloonAlertUtils.showErrorAlert(DevPilotMessageBundle.get("devpilot.alter.file.not.exist"), 0, -10, Balloon.Position.above); + return; + } + + VirtualFile file = baseDir.findFileByRelativePath(relativePath); + if (file == null || !file.exists()) { + BalloonAlertUtils.showErrorAlert(DevPilotMessageBundle.get("devpilot.alter.file.not.exist"), 0, -10, Balloon.Position.above); + return; + } + + FileEditorManager.getInstance(project).openTextEditor(new OpenFileDescriptor(project, file), true); + } } diff --git a/src/main/java/com/zhongan/devpilot/util/GitUtil.java b/src/main/java/com/zhongan/devpilot/util/GitUtil.java new file mode 100644 index 00000000..36ac6707 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/GitUtil.java @@ -0,0 +1,138 @@ +package com.zhongan.devpilot.util; + +import com.google.gson.Gson; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; + +import java.util.HashMap; +import java.util.Map; + +import git4idea.repo.GitRemote; +import git4idea.repo.GitRepository; +import okhttp3.Call; +import okhttp3.Request; +import okhttp3.Response; + +import static com.zhongan.devpilot.constant.DefaultConst.RAG_DEFAULT_HOST; + +public class GitUtil { + + private static final String ORIGIN = "origin"; + + private static final String SLASH = "/"; + + private static final String GIT = ".git"; + + private static final Map map = new HashMap<>(); + + private static final long syncTimeInterval = 6 * 60 * 60 * 1000; + + private static final String queryAppCodeEmbeddedStateUrl = RAG_DEFAULT_HOST; + + public static String getRepoNameFromFile(Project project, VirtualFile virtualFile) { + GitRepository repository = git4idea.GitUtil.getRepositoryManager(project).getRepositoryForFile(virtualFile); + if (repository == null) { + return null; + } + for (GitRemote remote : repository.getRemotes()) { + if (remote.getName().equals(ORIGIN)) { + String remoteUrl = remote.getFirstUrl(); + if (remoteUrl == null) { + return null; + } + + int lastSlashIndex = remoteUrl.lastIndexOf(SLASH); + int gitIndex = remoteUrl.lastIndexOf(GIT); + + if (lastSlashIndex == -1 || gitIndex == -1) { + return null; + } + + return remoteUrl.substring(lastSlashIndex + 1, gitIndex); + } + } + return null; + } + + public static Boolean isRepoEmbedded(String appName) { + Boolean embedded = Boolean.FALSE; + if (!map.containsKey(appName) || (System.currentTimeMillis() - map.get(appName).getTimestamp()) > syncTimeInterval) { + // 调用接口 + Response response; + try { + Request request = new Request.Builder() + .url(queryAppCodeEmbeddedStateUrl + appName) + .header("User-Agent", UserAgentUtils.buildUserAgent()) + .header("Auth-Type", LoginUtils.getLoginType()) + .get() + .build(); + + Call call = OkhttpUtils.getClient().newCall(request); + response = call.execute(); + + if (response.isSuccessful()) { + String responseBody = response.body().string(); + RepoEmbedded repoEmbedded = new Gson().fromJson(responseBody, RepoEmbedded.class); + embedded = repoEmbedded.getEmbedded(); + } + } catch (Exception e) { + return Boolean.FALSE; + } + + map.put(appName, new State(embedded, System.currentTimeMillis())); + } + return map.get(appName).getEmbedded(); + } + + public static class RepoEmbedded { + + private String repoName; + + private Boolean embedded; + + public Boolean getEmbedded() { + return embedded; + } + + public void setEmbedded(Boolean embedded) { + this.embedded = embedded; + } + + public String getRepoName() { + return repoName; + } + + public void setRepoName(String repoName) { + this.repoName = repoName; + } + } + + public static class State { + + private Boolean embedded; + + private Long timestamp; + + public State(Boolean embedded, Long timestamp) { + this.embedded = embedded; + this.timestamp = timestamp; + } + + public Long getTimestamp() { + return timestamp; + } + + public void setTimestamp(Long timestamp) { + this.timestamp = timestamp; + } + + public Boolean getEmbedded() { + return embedded; + } + + public void setEmbedded(Boolean embedded) { + this.embedded = embedded; + } + } + +} diff --git a/src/main/java/com/zhongan/devpilot/util/GitlabUtil.java b/src/main/java/com/zhongan/devpilot/util/GitlabUtil.java new file mode 100644 index 00000000..a37f1509 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/GitlabUtil.java @@ -0,0 +1,187 @@ +package com.zhongan.devpilot.util; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.containers.ContainerUtil; +import com.zhongan.devpilot.actions.notifications.DevPilotNotification; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.ini4j.Ini; +import org.ini4j.Profile; + +public class GitlabUtil { + + // DevPilotNotification.error(GitlabUtil.getRemoteUrl(project, FileDocumentManager.getInstance().getFile(editor.getDocument()))); + + public static String getRemoteUrl(Project project, VirtualFile file) { + String url = null; + try { + String configFilePath = getConfigFilePath(project.getBasePath(), file.getPath()); + Ini ini = loadIni(configFilePath); + url = getRemoteUrl(ini); + } catch (IOException e) { + DevPilotNotification.error("Error occurred while get remote url from config file" + e.getMessage()); + } + return url; + } + + private static String getConfigFilePath(String basePath, String filePath) { + String configFilePath = basePath + "/.git/config"; + File file = new File(configFilePath); + if (!file.exists()) { + Path prefixPath = Paths.get(basePath).toAbsolutePath(); + Path filePathAbsolute = Paths.get(filePath).toAbsolutePath(); + Path relativePath = prefixPath.relativize(filePathAbsolute); + Path targetPath = prefixPath.resolve(relativePath.subpath(0, 1)); + return getConfigFilePath(targetPath.toString(), filePath); + } + return configFilePath; + } + + private static Ini loadIni(String configFilePath) throws IOException { + Ini ini = new Ini(); + ini.load(new File(configFilePath)); + return ini; + } + + private static String getRemoteUrl(Ini ini) { + Pair, Collection> pairs = parseRemotes(ini); + Collection remotes = pairs.getFirst(); + + for (Remote remote : remotes) { + if ("origin".equalsIgnoreCase(remote.getName())) { + return remote.getUrls().iterator().next(); + } + } + + Remote firstRemote = remotes.iterator().next(); + return firstRemote.getUrls().iterator().next(); + } + + private static Pair, Collection> parseRemotes(Ini ini) { + Collection remotes = new ArrayList<>(); + Collection urls = new ArrayList<>(); + + for (String sectionName : ini.keySet()) { + Profile.Section section = ini.get(sectionName); + + Remote remote = parseRemote(sectionName, section); + if (remote != null) { + remotes.add(remote); + } else { + Url url = parseUrl(sectionName, section); + if (url != null) { + urls.add(url); + } + } + } + + return Pair.create(remotes, urls); + } + + private static final Pattern REMOTE_SECTION = Pattern.compile("(?:svn-)?remote \"(.*)\"", Pattern.CASE_INSENSITIVE); + + private static final Pattern URL_SECTION = Pattern.compile("url \"(.*)\"", Pattern.CASE_INSENSITIVE); + + private static Remote parseRemote(String sectionName, Profile.Section section) { + Matcher matcher = REMOTE_SECTION.matcher(sectionName); + if (matcher.matches() && matcher.groupCount() == 1) { + List fetch = ContainerUtil.notNullize(section.getAll("fetch")); + List push = ContainerUtil.notNullize(section.getAll("push")); + List url = ContainerUtil.notNullize(section.getAll("url")); + List pushUrl = ContainerUtil.notNullize(section.getAll("pushurl")); + return new Remote(matcher.group(1), fetch, push, url, pushUrl); + } + return null; + } + + private static Url parseUrl(String sectionName, Profile.Section section) { + Matcher matcher = URL_SECTION.matcher(sectionName); + if (matcher.matches() && matcher.groupCount() == 1) { + String insteadOf = section.get("insteadof"); + String pushInsteadOf = section.get("pushinsteadof"); + return new Url(matcher.group(1), insteadOf, pushInsteadOf); + } + return null; + } + + private static final class Remote { + + private final String name; + + List fetchSpecs; + + List pushSpec; + + List urls; + + List pushUrls; + + private Remote(String name, + List fetchSpecs, + List pushSpec, + List urls, + List pushUrls) { + this.name = name; + this.fetchSpecs = fetchSpecs; + this.pushSpec = pushSpec; + this.urls = urls; + this.pushUrls = pushUrls; + } + + private Collection getUrls() { + return urls; + } + + private Collection getPushUrls() { + return pushUrls; + } + + private List getPushSpec() { + return pushSpec; + } + + private List getFetchSpecs() { + return fetchSpecs; + } + + private String getName() { + return name; + } + + } + + private static final class Url { + private final String name; + + private final String insteadof; + + private final String pushInsteadof; + + private Url(String name, String insteadof, String pushInsteadof) { + this.name = name; + this.insteadof = insteadof; + this.pushInsteadof = pushInsteadof; + } + + public String getInsteadOf() { + return insteadof; + } + + public String getPushInsteadOf() { + return pushInsteadof; + } + + } + +} diff --git a/src/main/java/com/zhongan/devpilot/util/JetbrainsVersionUtils.java b/src/main/java/com/zhongan/devpilot/util/JetbrainsVersionUtils.java new file mode 100644 index 00000000..b3e42a22 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/JetbrainsVersionUtils.java @@ -0,0 +1,30 @@ +package com.zhongan.devpilot.util; + +import com.intellij.openapi.application.ApplicationInfo; + +public class JetbrainsVersionUtils { + public static String getJetbrainsBuildVersion() { + return ApplicationInfo.getInstance().getBuild().asString(); + } + + public static int getJetbrainsBaselineVersion() { + return ApplicationInfo.getInstance().getBuild().getBaselineVersion(); + } + + public static int[] getJetbrainsBuildVersionArr() { + return ApplicationInfo.getInstance().getBuild().getComponents(); + } + + // version >= 2023.3 (build version 233.11799.241) + public static boolean isVersionLaterThan233() { + var versionList = getJetbrainsBuildVersionArr(); + + var baseVersion = versionList[0]; + + if (baseVersion == 232) { + return versionList[1] >= 11799; + } + + return baseVersion > 232; + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/JsonUtils.java b/src/main/java/com/zhongan/devpilot/util/JsonUtils.java index 9a4212fb..d96a112c 100644 --- a/src/main/java/com/zhongan/devpilot/util/JsonUtils.java +++ b/src/main/java/com/zhongan/devpilot/util/JsonUtils.java @@ -1,10 +1,15 @@ package com.zhongan.devpilot.util; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; public class JsonUtils { private final static ObjectMapper objectMapper = new ObjectMapper(); + static { + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + public static String toJson(Object object) { try { return objectMapper.writeValueAsString(object); diff --git a/src/main/java/com/zhongan/devpilot/util/LanguageUtil.java b/src/main/java/com/zhongan/devpilot/util/LanguageUtil.java index 786c150d..be68718c 100644 --- a/src/main/java/com/zhongan/devpilot/util/LanguageUtil.java +++ b/src/main/java/com/zhongan/devpilot/util/LanguageUtil.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableSet; @@ -63,7 +64,8 @@ public static Language getLanguageByExtension(@Nullable String extensionName) { try { Map langMappings = new HashMap<>(); Map extMappings = new HashMap<>(); - ObjectMapper objectMapper = new ObjectMapper(); + ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); URL resource = LanguageUtil.class.getResource("/languageMappings.json"); List languages = objectMapper.readValue(resource, new TypeReference<>() { }); for (Language language : languages) { diff --git a/src/main/java/com/zhongan/devpilot/util/LoginUtils.java b/src/main/java/com/zhongan/devpilot/util/LoginUtils.java new file mode 100644 index 00000000..d87eb4b2 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/LoginUtils.java @@ -0,0 +1,113 @@ +package com.zhongan.devpilot.util; + +import com.intellij.ide.BrowserUtil; +import com.intellij.openapi.project.ProjectManager; +import com.zhongan.devpilot.constant.DefaultConst; +import com.zhongan.devpilot.enums.LoginTypeEnum; +import com.zhongan.devpilot.enums.ZaSsoEnum; +import com.zhongan.devpilot.gui.toolwindows.chat.DevPilotChatToolWindowService; +import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; +import com.zhongan.devpilot.settings.state.TrialServiceSettingsState; +import com.zhongan.devpilot.statusBar.DevPilotStatusBarBaseWidget; +import com.zhongan.devpilot.statusBar.status.DevPilotStatusEnum; + +import java.util.Locale; + +import org.jetbrains.ide.BuiltInServerManager; + +import static com.zhongan.devpilot.constant.DefaultConst.AUTH_ON; +import static com.zhongan.devpilot.constant.DefaultConst.LOGIN_AUTH_URL; +import static com.zhongan.devpilot.constant.DefaultConst.LOGIN_CALLBACK_URL; + +public class LoginUtils { + public static String loginUrl() { + var callback = String.format(LOGIN_CALLBACK_URL, BuiltInServerManager.getInstance().getPort()); + return String.format(LOGIN_AUTH_URL, callback, DefaultConst.DEFAULT_SOURCE_STRING); + } + + public static void gotoLogin() { + BrowserUtil.browse(loginUrl()); + } + + public static String getLoginType() { + if (!isAuthOn()) { + return ""; + } + + var setting = DevPilotLlmSettingsState.getInstance(); + return setting.getLoginType().toLowerCase(Locale.ROOT); + } + + public static void changeLoginStatus(boolean isLoggedIn) { + var projects = ProjectManager.getInstance().getOpenProjects(); + + for (var project : projects) { + var service = project.getService(DevPilotChatToolWindowService.class); + service.changeLoginStatus(isLoggedIn); + DevPilotStatusBarBaseWidget.update(project, isLoggedIn ? DevPilotStatusEnum.LoggedIn : DevPilotStatusEnum.NotLoggedIn); + } + } + + public static boolean isLogin() { + if (!isAuthOn()) { + return true; + } + + var setting = DevPilotLlmSettingsState.getInstance(); + var loginType = LoginTypeEnum.getLoginTypeEnum(setting.getLoginType()); + + switch (loginType) { + case WX: + return WxAuthUtils.isLogin(); + case ZA: + return ZaSsoUtils.isLogin(ZaSsoEnum.ZA); + case ZA_TI: + return ZaSsoUtils.isLogin(ZaSsoEnum.ZA_TI); + default: + return false; + } + } + + public static void logout() { + if (!isAuthOn()) { + return; + } + + var setting = DevPilotLlmSettingsState.getInstance(); + var loginType = LoginTypeEnum.getLoginTypeEnum(setting.getLoginType()); + + switch (loginType) { + case WX: + WxAuthUtils.logout(); + break; + case ZA: + ZaSsoUtils.logout(ZaSsoEnum.ZA); + break; + case ZA_TI: + ZaSsoUtils.logout(ZaSsoEnum.ZA_TI); + break; + } + + changeLoginStatus(false); + } + + public static String getUsername() { + if (!isAuthOn()) { + return "user"; + } + + if ("wx".equals(LoginUtils.getLoginType())) { + String prefix = DevPilotMessageBundle.get("devpilot.status.account.wx"); + String wxUserId = TrialServiceSettingsState.getInstance().getWxUserId(); + return prefix + wxUserId.substring(wxUserId.length() - 4); + + } else { + return ZaSsoUtils.getSsoUserName(); + } + } + + // is auth turn on + public static boolean isAuthOn() { + return AUTH_ON; + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/NewFileUtils.java b/src/main/java/com/zhongan/devpilot/util/NewFileUtils.java index b51650bc..bf3b55e6 100644 --- a/src/main/java/com/zhongan/devpilot/util/NewFileUtils.java +++ b/src/main/java/com/zhongan/devpilot/util/NewFileUtils.java @@ -26,20 +26,22 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; public class NewFileUtils { - public static void createNewFile(Project project, String generatedText, CodeReferenceModel codeReferenceModel) { - if (codeReferenceModel == null) { + public static void createNewFile(Project project, String generatedText, CodeReferenceModel codeReferenceModel, String lang) { + // if lang is null, set default language to java + if (StringUtils.isEmpty(lang)) { handleDefaultAction(project, generatedText, ".java"); - return; } - var fileUrl = codeReferenceModel.getFileUrl(); - var fileName = codeReferenceModel.getFileName(); + String fileExtension = MarkdownUtil.getFileExtensionFromLanguage(lang); - String fileExtension = fileName.substring(fileName.lastIndexOf(".")); - if (".java".equals(fileExtension) && codeReferenceModel.getType() == EditorActionEnum.GENERATE_TESTS) { + if (codeReferenceModel != null && ".java".equals(fileExtension) + && codeReferenceModel.getType() == EditorActionEnum.GENERATE_TESTS) { + var fileUrl = codeReferenceModel.getFileUrl(); + var fileName = codeReferenceModel.getFileName(); // java test will goto special logic handleGenerateTestsAction(project, generatedText, fileExtension, fileName, fileUrl); } else { diff --git a/src/main/java/com/zhongan/devpilot/util/PerformanceCheckUtils.java b/src/main/java/com/zhongan/devpilot/util/PerformanceCheckUtils.java index 93a345c2..7e9956fa 100644 --- a/src/main/java/com/zhongan/devpilot/util/PerformanceCheckUtils.java +++ b/src/main/java/com/zhongan/devpilot/util/PerformanceCheckUtils.java @@ -1,5 +1,6 @@ package com.zhongan.devpilot.util; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; import com.intellij.diff.DiffContentFactory; @@ -17,8 +18,10 @@ import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; +import com.zhongan.devpilot.actions.notifications.DevPilotNotification; import com.zhongan.devpilot.integrations.llms.LlmProviderFactory; import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionRequest; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotChatCompletionResponse; import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; import com.zhongan.devpilot.integrations.llms.entity.PerformanceCheckResponse; @@ -37,7 +40,8 @@ public class PerformanceCheckUtils { public static final List NO_PERFORMANCE_ISSUES = Lists.newArrayList(NO_PERFORMANCE_ISSUES_DESC, NO_PERFORMANCE_ISSUES_NULL); - private final static ObjectMapper objectMapper = new ObjectMapper(); + private final static ObjectMapper objectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); private static final String CUSTOM_PROMPT = "Please optimize the code above for performance. " + "Provide two outputs: one as 'null' indicating no performance issues, " + @@ -58,9 +62,14 @@ public static String getChatCompletionResult(String selectedText, Project projec DevPilotChatCompletionRequest request = new DevPilotChatCompletionRequest(); // list content support update request.setMessages(new ArrayList<>() {{ add(devPilotMessage); }}); - final String response = new LlmProviderFactory().getLlmProvider(project).chatCompletion(project, request, null); + + final DevPilotChatCompletionResponse response = new LlmProviderFactory().getLlmProvider(project).chatCompletionSync(request); try { - PerformanceCheckResponse performanceCheckResponse = objectMapper.readValue(response, PerformanceCheckResponse.class); + DevPilotNotification.debug("Getting PerformanceCheckResponse is [" + response.isSuccessful() + "], content is [" + response.getContent() + "]."); + if (!response.isSuccessful()) { + return selectedText; + } + PerformanceCheckResponse performanceCheckResponse = objectMapper.readValue(response.getContent(), PerformanceCheckResponse.class); if (StringUtils.isEmpty(performanceCheckResponse.getRewriteCode())) { return selectedText; } @@ -89,7 +98,7 @@ public static void showDiff(Project project, Editor editor, VirtualFile original DiffContent replaceContent = DiffEditorUtils.getDiffContent(diffContentFactory, project, replaceDocument); DiffContent originalContent = DiffEditorUtils.getDiffContent(diffContentFactory, project, editor.getDocument()); DiffRequest diffRequest = new SimpleDiffRequest("Dev Pilot: Diff view", - replaceContent, originalContent, "Dev Pilot suggested code", originalFile.getName() + "(original code)"); + replaceContent, originalContent, "Dev Pilot suggested code", originalFile.getName() + "(original code)"); DiffManager diffManager = DiffManager.getInstance(); diffManager.showDiff(project, diffRequest); EditorFactory.getInstance().addEditorFactoryListener(new EditorFactoryListener() { diff --git a/src/main/java/com/zhongan/devpilot/util/RestServiceUtils.java b/src/main/java/com/zhongan/devpilot/util/RestServiceUtils.java new file mode 100644 index 00000000..c462c260 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/RestServiceUtils.java @@ -0,0 +1,27 @@ +package com.zhongan.devpilot.util; + +import com.zhongan.devpilot.webview.model.CallbackUserInfo; + +import io.netty.handler.codec.http.QueryStringDecoder; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class RestServiceUtils { + public static String getParameter(QueryStringDecoder queryStringDecoder, String key) { + var list = queryStringDecoder.parameters().get(key); + if (list == null || list.isEmpty()) { + return null; + } + return list.get(0); + } + + public static CallbackUserInfo parseToken(String token) { + var decodedBytes = Base64.getDecoder().decode(token); + var decodedString = new String(decodedBytes); + var decodedUrl = URLDecoder.decode(decodedString, StandardCharsets.UTF_8); + + return JsonUtils.fromJson(decodedUrl, CallbackUserInfo.class); + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/TelemetryUtils.java b/src/main/java/com/zhongan/devpilot/util/TelemetryUtils.java new file mode 100644 index 00000000..8f3b2dcd --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/TelemetryUtils.java @@ -0,0 +1,209 @@ +package com.zhongan.devpilot.util; + +import com.intellij.psi.PsiFile; +import com.zhongan.devpilot.enums.ChatActionTypeEnum; +import com.zhongan.devpilot.webview.model.CodeActionModel; + +import java.io.IOException; +import java.util.Locale; + +import org.apache.commons.lang3.StringUtils; + +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; + +import static com.zhongan.devpilot.constant.DefaultConst.TELEMETRY_CHAT_ACCEPT_PATH; +import static com.zhongan.devpilot.constant.DefaultConst.TELEMETRY_COMPLETION_ACCEPT_PATH; +import static com.zhongan.devpilot.constant.DefaultConst.TELEMETRY_HOST; +import static com.zhongan.devpilot.constant.DefaultConst.TELEMETRY_LIKE_PATH; +import static com.zhongan.devpilot.constant.DefaultConst.TELEMETRY_ON; + +public class TelemetryUtils { + + public static void messageFeedback(String id, boolean action) { + if (!isTelemetryTurnOn()) { + return; + } + + var feedbackRequest = new FeedbackRequest(action); + var requestJson = JsonUtils.toJson(feedbackRequest); + + if (requestJson == null) { + return; + } + + var path = String.format(TELEMETRY_LIKE_PATH, id); + var url = TELEMETRY_HOST + path; + + sendMessage(url, requestJson); + } + + public static void chatAccept(CodeActionModel codeActionModel, ChatActionTypeEnum actionType) { + if (!isTelemetryTurnOn()) { + return; + } + + chatAccept(codeActionModel.getMessageId(), codeActionModel.getContent(), codeActionModel.getLang(), actionType); + } + + public static void chatAccept(String id, String acceptLines, String language, ChatActionTypeEnum actionType) { + // if language is null return text + if (StringUtils.isEmpty(language)) { + language = "text"; + } + + var chatAcceptRequest = new ChatAcceptRequest(acceptLines, + language.toLowerCase(Locale.ROOT), actionType.getType()); + var requestJson = JsonUtils.toJson(chatAcceptRequest); + + if (requestJson == null) { + return; + } + + var path = String.format(TELEMETRY_CHAT_ACCEPT_PATH, id); + var url = TELEMETRY_HOST + path; + + sendMessage(url, requestJson); + } + + public static void completionAccept(String id, PsiFile file) { + if (!isTelemetryTurnOn()) { + return; + } + + var name = file.getName(); + var fileExtension = name.substring(name.lastIndexOf(".") + 1); + + var lang = LanguageUtil.getLanguageByExtension(fileExtension); + + String language = null; + + if (lang != null) { + language = lang.getLanguageName(); + } + + completionAccept(id, language); + } + + public static void completionAccept(String id, String language) { + if (!isTelemetryTurnOn()) { + return; + } + + // if language is null return text + if (StringUtils.isEmpty(language)) { + language = "text"; + } + + var completionAcceptRequest = new CompletionAcceptRequest(language.toLowerCase(Locale.ROOT)); + var requestJson = JsonUtils.toJson(completionAcceptRequest); + + if (requestJson == null) { + return; + } + + var path = String.format(TELEMETRY_COMPLETION_ACCEPT_PATH, id); + var url = TELEMETRY_HOST + path; + + sendMessage(url, requestJson); + } + + public static void sendMessage(String url, String requestJson) { + var client = OkhttpUtils.getClient(); + + okhttp3.Response response = null; + + try { + var request = new Request.Builder() + .url(url) + .header("Auth-Type", LoginUtils.getLoginType()) + .header("User-Agent", UserAgentUtils.buildUserAgent()) + .put(RequestBody.create(requestJson, MediaType.parse("application/json"))) + .build(); + + var call = client.newCall(request); + response = call.execute(); + } catch (IOException e) { + // ignore error + } finally { + if (response != null) { + response.close(); + } + } + } + + public static boolean isTelemetryTurnOn() { + return TELEMETRY_ON; + } + + static class FeedbackRequest { + private Boolean agreeStatus; + + FeedbackRequest(Boolean agreeStatus) { + this.agreeStatus = agreeStatus; + } + + public Boolean getAgreeStatus() { + return agreeStatus; + } + + public void setAgreeStatus(Boolean agreeStatus) { + this.agreeStatus = agreeStatus; + } + } + + static class ChatAcceptRequest { + private String acceptedLines; + + private String language; + + private String actionType; + + ChatAcceptRequest(String acceptedLines, String language, String actionType) { + this.acceptedLines = acceptedLines; + this.language = language; + this.actionType = actionType; + } + + public String getAcceptedLines() { + return acceptedLines; + } + + public void setAcceptedLines(String acceptedLines) { + this.acceptedLines = acceptedLines; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public String getActionType() { + return actionType; + } + + public void setActionType(String actionType) { + this.actionType = actionType; + } + } + + static class CompletionAcceptRequest { + private String language; + + CompletionAcceptRequest(String language) { + this.language = language; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/ThemeUtils.java b/src/main/java/com/zhongan/devpilot/util/ThemeUtils.java new file mode 100644 index 00000000..a451a629 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/ThemeUtils.java @@ -0,0 +1,18 @@ +package com.zhongan.devpilot.util; + +import javax.swing.UIManager; + +public class ThemeUtils { + public static boolean isDarkTheme() { + if (JetbrainsVersionUtils.isVersionLaterThan233()) { + var lookAndFeelDefaults = UIManager.getLookAndFeelDefaults(); + return lookAndFeelDefaults == null || lookAndFeelDefaults.getBoolean("ui.theme.is.dark"); + } + + return UIManager.getLookAndFeel().getName().contains("Darcula"); + } + + public static String themeType() { + return isDarkTheme() ? "dark" : "light"; + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/TokenUtils.java b/src/main/java/com/zhongan/devpilot/util/TokenUtils.java new file mode 100644 index 00000000..db4571c2 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/TokenUtils.java @@ -0,0 +1,47 @@ +package com.zhongan.devpilot.util; + +import com.knuddels.jtokkit.Encodings; +import com.knuddels.jtokkit.api.Encoding; +import com.knuddels.jtokkit.api.ModelType; +import com.zhongan.devpilot.constant.DefaultConst; +import com.zhongan.devpilot.constant.PromptConst; +import com.zhongan.devpilot.integrations.llms.entity.DevPilotMessage; + +import java.util.ArrayList; +import java.util.List; + +public class TokenUtils { + + private static final Encoding GPT_3_5_TURBO_16K_ENC = Encodings.newDefaultEncodingRegistry() + .getEncodingForModel(ModelType.GPT_3_5_TURBO_16K); + + public static List ComputeTokensFromMessagesUsingGPT35Enc(List messages) { + List tokensCount = new ArrayList<>(messages.size()); + for (DevPilotMessage message : messages) { + tokensCount.add(GPT_3_5_TURBO_16K_ENC.countTokensOrdinary(message.getContent())); + } + return tokensCount; + } + + public static Integer ComputeTokensFromContentUsingGTP35Enc(String content) { + return GPT_3_5_TURBO_16K_ENC.countTokensOrdinary(content); + } + + /** + * check length of input rather than max limit + */ + public static boolean isInputExceedLimit(String content, String prompt) { + // text too long, openai server always timeout + String userPrompt = prompt.replace("{{selectedCode}}", content); + + return TokenUtils.ComputeTokensFromContentUsingGTP35Enc(userPrompt) + + TokenUtils.ComputeTokensFromContentUsingGTP35Enc(PromptConst.RESPONSE_FORMAT) > DefaultConst.GPT_35_TOKEN_MAX_LENGTH; + } + + public static boolean isInputExceedLimit(String userPrompt) { + // text too long, openai server always timeout + return TokenUtils.ComputeTokensFromContentUsingGTP35Enc(userPrompt) + + TokenUtils.ComputeTokensFromContentUsingGTP35Enc(PromptConst.RESPONSE_FORMAT) > DefaultConst.GPT_35_TOKEN_MAX_LENGTH; + } + +} diff --git a/src/main/java/com/zhongan/devpilot/util/UserAgentUtils.java b/src/main/java/com/zhongan/devpilot/util/UserAgentUtils.java index b226457e..d8154bc8 100644 --- a/src/main/java/com/zhongan/devpilot/util/UserAgentUtils.java +++ b/src/main/java/com/zhongan/devpilot/util/UserAgentUtils.java @@ -1,12 +1,56 @@ package com.zhongan.devpilot.util; import com.zhongan.devpilot.DevPilotVersion; +import com.zhongan.devpilot.enums.LoginTypeEnum; +import com.zhongan.devpilot.enums.ZaSsoEnum; +import com.zhongan.devpilot.settings.state.AIGatewaySettingsState; import com.zhongan.devpilot.settings.state.DevPilotLlmSettingsState; +import com.zhongan.devpilot.settings.state.TrialServiceSettingsState; public class UserAgentUtils { - public static String getUserAgent() { - // format: idea version|plugin version|uuid - return String.format("%s|%s|%s", DevPilotVersion.getIdeaVersion(), - DevPilotVersion.getDevPilotVersion(), DevPilotLlmSettingsState.getInstance().getUuid()); + public static String buildUserAgent() { + var settings = DevPilotLlmSettingsState.getInstance(); + var loginType = LoginTypeEnum.getLoginTypeEnum(settings.getLoginType()); + + switch (loginType) { + case ZA: + return getUserAgent(ZaSsoEnum.ZA); + case ZA_TI: + return getUserAgent(ZaSsoEnum.ZA_TI); + case WX: + return getWxUserAgent(); + } + + return getWxUserAgent(); + } + + public static String getUserAgent(ZaSsoEnum zaSsoEnum) { + String token, username; + + var settings = AIGatewaySettingsState.getInstance(); + + switch (zaSsoEnum) { + case ZA_TI: + token = settings.getTiSsoToken(); + username = settings.getTiSsoUsername(); + break; + case ZA: + default: + token = settings.getSsoToken(); + username = settings.getSsoUsername(); + break; + } + + // format: idea version|plugin version|user token|username + return String.format("%s-%s|%s|%s|%s", DevPilotVersion.getVersionName(), DevPilotVersion.getIdeaVersion(), + DevPilotVersion.getDevPilotVersion(), token, username); + } + + public static String getWxUserAgent() { + var settings = TrialServiceSettingsState.getInstance(); + + // format: idea version|plugin version|token|userid + return String.format("%s-%s|%s|%s|%s", DevPilotVersion.getVersionName(), DevPilotVersion.getIdeaVersion(), + DevPilotVersion.getDevPilotVersion(), settings.getWxToken(), settings.getWxUserId()); } } diff --git a/src/main/java/com/zhongan/devpilot/util/VirtualFileUtil.java b/src/main/java/com/zhongan/devpilot/util/VirtualFileUtil.java index d628b3db..e4543ac3 100644 --- a/src/main/java/com/zhongan/devpilot/util/VirtualFileUtil.java +++ b/src/main/java/com/zhongan/devpilot/util/VirtualFileUtil.java @@ -3,8 +3,14 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ModuleRootManager; +import com.intellij.openapi.roots.ProjectFileIndex; import com.intellij.openapi.util.Computable; +import com.intellij.openapi.vcs.ProjectLevelVcsManager; +import com.intellij.openapi.vcs.VcsRoot; +import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; @@ -44,4 +50,29 @@ public static VirtualFile createVirtualReplaceFile(Editor editor, Project projec }); } + public static String getRelativeFilePath(Project project, VirtualFile virtualFile) { + VcsRoot vcsRoot = ProjectLevelVcsManager.getInstance(project).getVcsRootObjectFor(virtualFile); + if (vcsRoot != null) { + return VfsUtilCore.getRelativePath(virtualFile, vcsRoot.getPath()); + } + + Module module = ProjectFileIndex.getInstance(project).getModuleForFile(virtualFile, false); + if (module != null) { + VirtualFile[] roots = ModuleRootManager.getInstance(module).getContentRoots(); + for (VirtualFile root : roots) { + String relativePath = VfsUtilCore.getRelativePath(virtualFile, root); + if (relativePath != null) { + return relativePath; + } + } + } + + VirtualFile sourceRoot = ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile); + if (sourceRoot != null) { + return VfsUtilCore.getRelativePath(virtualFile, sourceRoot); + } + + return null; + } + } \ No newline at end of file diff --git a/src/main/java/com/zhongan/devpilot/util/WxAuthUtils.java b/src/main/java/com/zhongan/devpilot/util/WxAuthUtils.java new file mode 100644 index 00000000..044ca26d --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/WxAuthUtils.java @@ -0,0 +1,28 @@ +package com.zhongan.devpilot.util; + +import com.zhongan.devpilot.settings.state.TrialServiceSettingsState; + +import org.apache.commons.lang3.StringUtils; + +public class WxAuthUtils { + public static boolean isLogin() { + var settings = TrialServiceSettingsState.getInstance(); + return StringUtils.isNotBlank(settings.getWxToken()) + && StringUtils.isNotBlank(settings.getWxUsername()) + && StringUtils.isNotBlank(settings.getWxUserId()); + } + + public static void logout() { + var setting = TrialServiceSettingsState.getInstance(); + setting.setWxToken(null); + setting.setWxUsername(null); + setting.setWxUserId(null); + } + + public static void login(String token, String username, String userId) { + var setting = TrialServiceSettingsState.getInstance(); + setting.setWxToken(token); + setting.setWxUsername(username); + setting.setWxUserId(userId); + } +} diff --git a/src/main/java/com/zhongan/devpilot/util/ZaSsoUtils.java b/src/main/java/com/zhongan/devpilot/util/ZaSsoUtils.java new file mode 100644 index 00000000..4f33f5e3 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/util/ZaSsoUtils.java @@ -0,0 +1,77 @@ +package com.zhongan.devpilot.util; + +import com.zhongan.devpilot.enums.ZaSsoEnum; +import com.zhongan.devpilot.settings.state.AIGatewaySettingsState; + +import java.util.Locale; + +import org.apache.commons.lang3.StringUtils; + +public class ZaSsoUtils { + public static boolean isLogin(ZaSsoEnum zaSsoEnum) { + var settings = AIGatewaySettingsState.getInstance(); + switch (zaSsoEnum) { + case ZA_TI: + return StringUtils.isNotBlank(settings.getTiSsoToken()) && StringUtils.isNotBlank(settings.getTiSsoUsername()); + case ZA: + default: + return StringUtils.isNotBlank(settings.getSsoToken()) && StringUtils.isNotBlank(settings.getSsoUsername()); + } + } + + public static void login(ZaSsoEnum zaSsoEnum, String token, String username) { + var settings = AIGatewaySettingsState.getInstance(); + settings.setSelectedSso(zaSsoEnum.getName()); + switch (zaSsoEnum) { + case ZA_TI: + settings.setTiSsoToken(token); + settings.setTiSsoUsername(username); + break; + case ZA: + default: + settings.setSsoToken(token); + settings.setSsoUsername(username); + break; + } + } + + public static String zaSsoUsername(ZaSsoEnum zaSsoEnum) { + var settings = AIGatewaySettingsState.getInstance(); + switch (zaSsoEnum) { + case ZA_TI: + return settings.getTiSsoUsername(); + case ZA: + default: + return settings.getSsoUsername(); + } + } + + public static void logout(ZaSsoEnum zaSsoEnum) { + var settings = AIGatewaySettingsState.getInstance(); + switch (zaSsoEnum) { + case ZA_TI: + settings.setTiSsoUsername(null); + settings.setTiSsoToken(null); + break; + case ZA: + default: + settings.setSsoUsername(null); + settings.setSsoToken(null); + break; + } + } + + public static String getSsoType() { + var settings = AIGatewaySettingsState.getInstance(); + return settings.getSelectedSso().toLowerCase(Locale.ROOT); + } + + public static ZaSsoEnum getSsoEnum() { + var settings = AIGatewaySettingsState.getInstance(); + return ZaSsoEnum.fromName(settings.getSelectedSso()); + } + + public static String getSsoUserName() { + return zaSsoUsername(getSsoEnum()); + } +} diff --git a/src/main/java/com/zhongan/devpilot/webview/model/CallbackUserInfo.java b/src/main/java/com/zhongan/devpilot/webview/model/CallbackUserInfo.java new file mode 100644 index 00000000..66efbfcd --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/webview/model/CallbackUserInfo.java @@ -0,0 +1,111 @@ +package com.zhongan.devpilot.webview.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Login Callback + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class CallbackUserInfo { + private long companyId; + + private String companyName; + + private long departmentId; + + private String email; + + private long id; + + private String token; + + private String username; + + private String userType; + + // 公众号登录 + private String nickname; + + // 公众号登录 + private String openid; + + public long getCompanyId() { + return companyId; + } + + public void setCompanyId(long companyId) { + this.companyId = companyId; + } + + public String getCompanyName() { + return companyName; + } + + public void setCompanyName(String companyName) { + this.companyName = companyName; + } + + public long getDepartmentId() { + return departmentId; + } + + public void setDepartmentId(long departmentId) { + this.departmentId = departmentId; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getUserType() { + return userType; + } + + public void setUserType(String userType) { + this.userType = userType; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + public String getOpenid() { + return openid; + } + + public void setOpenid(String openid) { + this.openid = openid; + } +} diff --git a/src/main/java/com/zhongan/devpilot/webview/model/CodeActionModel.java b/src/main/java/com/zhongan/devpilot/webview/model/CodeActionModel.java index 386f6968..9a2e1dd8 100644 --- a/src/main/java/com/zhongan/devpilot/webview/model/CodeActionModel.java +++ b/src/main/java/com/zhongan/devpilot/webview/model/CodeActionModel.java @@ -1,10 +1,17 @@ package com.zhongan.devpilot.webview.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) public class CodeActionModel { private String content; private String messageId; + private String lang; + + private String repo; + public String getContent() { return content; } @@ -20,4 +27,20 @@ public String getMessageId() { public void setMessageId(String messageId) { this.messageId = messageId; } + + public String getLang() { + return lang; + } + + public void setLang(String lang) { + this.lang = lang; + } + + public String getRepo() { + return repo; + } + + public void setRepo(String repo) { + this.repo = repo; + } } diff --git a/src/main/java/com/zhongan/devpilot/webview/model/CodeReferenceModel.java b/src/main/java/com/zhongan/devpilot/webview/model/CodeReferenceModel.java index 635bc733..b29d3be0 100644 --- a/src/main/java/com/zhongan/devpilot/webview/model/CodeReferenceModel.java +++ b/src/main/java/com/zhongan/devpilot/webview/model/CodeReferenceModel.java @@ -1,8 +1,10 @@ package com.zhongan.devpilot.webview.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.zhongan.devpilot.enums.EditorActionEnum; +@JsonIgnoreProperties(ignoreUnknown = true) public class CodeReferenceModel { private String fileUrl; diff --git a/src/main/java/com/zhongan/devpilot/webview/model/ConfigModel.java b/src/main/java/com/zhongan/devpilot/webview/model/ConfigModel.java index cc6782b0..562115a2 100644 --- a/src/main/java/com/zhongan/devpilot/webview/model/ConfigModel.java +++ b/src/main/java/com/zhongan/devpilot/webview/model/ConfigModel.java @@ -1,5 +1,8 @@ package com.zhongan.devpilot.webview.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) public class ConfigModel { private String theme; @@ -7,10 +10,23 @@ public class ConfigModel { private String username; - public ConfigModel(String theme, String locale, String username) { + private boolean loggedIn; + + private String env; + + private String version; + + private String platform; + + public ConfigModel(String theme, String locale, String username, + boolean loggedIn, String env, String version, String platform) { this.theme = theme; this.locale = locale; this.username = username; + this.loggedIn = loggedIn; + this.env = env; + this.version = version; + this.platform = platform; } public String getTheme() { @@ -36,4 +52,36 @@ public String getUsername() { public void setUsername(String username) { this.username = username; } + + public boolean isLoggedIn() { + return loggedIn; + } + + public void setLoggedIn(boolean loggedIn) { + this.loggedIn = loggedIn; + } + + public String getEnv() { + return env; + } + + public void setEnv(String env) { + this.env = env; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } } diff --git a/src/main/java/com/zhongan/devpilot/webview/model/CopyModel.java b/src/main/java/com/zhongan/devpilot/webview/model/CopyModel.java deleted file mode 100644 index 5c9822d3..00000000 --- a/src/main/java/com/zhongan/devpilot/webview/model/CopyModel.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.zhongan.devpilot.webview.model; - -public class CopyModel { - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } -} diff --git a/src/main/java/com/zhongan/devpilot/webview/model/EmbeddedModel.java b/src/main/java/com/zhongan/devpilot/webview/model/EmbeddedModel.java new file mode 100644 index 00000000..2b9531f0 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/webview/model/EmbeddedModel.java @@ -0,0 +1,32 @@ +package com.zhongan.devpilot.webview.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class EmbeddedModel { + + private Boolean repoEmbedded; + + private String repoName; + + public EmbeddedModel(Boolean embedded, String repoName) { + this.repoEmbedded = embedded; + this.repoName = repoName; + } + + public Boolean getRepoEmbedded() { + return repoEmbedded; + } + + public void setRepoEmbedded(Boolean repoEmbedded) { + this.repoEmbedded = repoEmbedded; + } + + public String getRepoName() { + return repoName; + } + + public void setRepoName(String repoName) { + this.repoName = repoName; + } +} diff --git a/src/main/java/com/zhongan/devpilot/webview/model/JavaCallModel.java b/src/main/java/com/zhongan/devpilot/webview/model/JavaCallModel.java index 755ecee9..e79e83fb 100644 --- a/src/main/java/com/zhongan/devpilot/webview/model/JavaCallModel.java +++ b/src/main/java/com/zhongan/devpilot/webview/model/JavaCallModel.java @@ -1,5 +1,8 @@ package com.zhongan.devpilot.webview.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) public class JavaCallModel { private String command; diff --git a/src/main/java/com/zhongan/devpilot/webview/model/JsCallModel.java b/src/main/java/com/zhongan/devpilot/webview/model/JsCallModel.java index 184d2d1b..8301d21a 100644 --- a/src/main/java/com/zhongan/devpilot/webview/model/JsCallModel.java +++ b/src/main/java/com/zhongan/devpilot/webview/model/JsCallModel.java @@ -1,5 +1,8 @@ package com.zhongan.devpilot.webview.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) public class JsCallModel { private String command; diff --git a/src/main/java/com/zhongan/devpilot/webview/model/LocaleModel.java b/src/main/java/com/zhongan/devpilot/webview/model/LocaleModel.java index 611f57e8..83f844e9 100644 --- a/src/main/java/com/zhongan/devpilot/webview/model/LocaleModel.java +++ b/src/main/java/com/zhongan/devpilot/webview/model/LocaleModel.java @@ -1,5 +1,8 @@ package com.zhongan.devpilot.webview.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) public class LocaleModel { private String locale; diff --git a/src/main/java/com/zhongan/devpilot/webview/model/LoginModel.java b/src/main/java/com/zhongan/devpilot/webview/model/LoginModel.java new file mode 100644 index 00000000..22eafdd1 --- /dev/null +++ b/src/main/java/com/zhongan/devpilot/webview/model/LoginModel.java @@ -0,0 +1,20 @@ +package com.zhongan.devpilot.webview.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class LoginModel { + private boolean loggedIn; + + public LoginModel(boolean loggedIn) { + this.loggedIn = loggedIn; + } + + public boolean isLoggedIn() { + return loggedIn; + } + + public void setLoggedIn(boolean loggedIn) { + this.loggedIn = loggedIn; + } +} diff --git a/src/main/java/com/zhongan/devpilot/webview/model/MessageModel.java b/src/main/java/com/zhongan/devpilot/webview/model/MessageModel.java index f2e718bb..70ca24ea 100644 --- a/src/main/java/com/zhongan/devpilot/webview/model/MessageModel.java +++ b/src/main/java/com/zhongan/devpilot/webview/model/MessageModel.java @@ -40,7 +40,7 @@ public static MessageModel buildUserMessage(String id, Long time, String content return buildCodeMessage(id, time, content, username, null); } - public static MessageModel buildErrorMessage(String content) { + public static MessageModel buildInfoMessage(String content) { return buildAssistantMessage(UUID.randomUUID().toString(), System.currentTimeMillis(), content, false); } diff --git a/src/main/java/com/zhongan/devpilot/webview/model/ThemeModel.java b/src/main/java/com/zhongan/devpilot/webview/model/ThemeModel.java index 9dd55797..5e2308f0 100644 --- a/src/main/java/com/zhongan/devpilot/webview/model/ThemeModel.java +++ b/src/main/java/com/zhongan/devpilot/webview/model/ThemeModel.java @@ -1,5 +1,8 @@ package com.zhongan.devpilot.webview.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) public class ThemeModel { private String theme; diff --git a/src/main/resources/META-INF/plugin-withGolang.xml b/src/main/resources/META-INF/plugin-withGolang.xml new file mode 100644 index 00000000..21fc1e86 --- /dev/null +++ b/src/main/resources/META-INF/plugin-withGolang.xml @@ -0,0 +1,10 @@ + + Zhongan Online + + com.intellij.modules.lang + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin-withJava.xml b/src/main/resources/META-INF/plugin-withJava.xml new file mode 100644 index 00000000..0c9e8f5d --- /dev/null +++ b/src/main/resources/META-INF/plugin-withJava.xml @@ -0,0 +1,10 @@ + + Zhongan Online + + com.intellij.modules.lang + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin-withJavaScript.xml b/src/main/resources/META-INF/plugin-withJavaScript.xml new file mode 100644 index 00000000..ce6b0293 --- /dev/null +++ b/src/main/resources/META-INF/plugin-withJavaScript.xml @@ -0,0 +1,11 @@ + + Zhongan Online + + com.intellij.modules.lang + + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin-withPython.xml b/src/main/resources/META-INF/plugin-withPython.xml new file mode 100644 index 00000000..3f91073f --- /dev/null +++ b/src/main/resources/META-INF/plugin-withPython.xml @@ -0,0 +1,10 @@ + + Zhongan Online + + com.intellij.modules.lang + + + + + + \ 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 f5f110c8..99346c29 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -6,6 +6,60 @@ DevPilot + 2.4.2 + + Release 2.4.2 +

New Features

+
    +
  • Support shortcut on methods for go | js | ts | py
  • +
  • Add intelligent handling of chat context
  • +
+

Enhancements

+
    +
  • Optimize method comment insertion operation
  • +
+

Bug fixes

+
    +
  • Fix autocompletion failure due to missing fonts in some ide versions
  • +
+

Release 2.4.1

+

New Features

+
    +
  • Interactive optimization
  • +
  • Add inlay hint for Comments | Summary
  • +
  • Optimize status bar action behavior
  • +
+

Enhancements

+
    +
  • Updated prompts for code explanation, code fixes, performance checks, and code reviews
  • +
  • Optimized autocompletion performance
  • +
+

Release 2.4.0

+

New Features

+
    +
  • Interactive optimization
  • +
  • Add inlay hint for Explain | Fix | Test
  • +
  • Optimize status bar action behavior
  • +
+

Release 2.3.0

+

Enhancements

+
    +
  • Enhance the plugin compatibility with LLM responses
  • +
+

Release 2.2.0

+

New Features

+
    +
  • Supports free trial for users who follow our Weixin Official Account by scanning QR code
  • +
  • Supports streaming response conversations
  • +
  • Supports generating commit logs based on change records
  • +
  • Supports single-round dialogue removal
  • +
  • Optimize dialogue interaction style
  • +
  • Supports Command Line mode
  • +
  • Some models support user feedback on likes and dislikes of results
  • +
  • Some models support line-level completion of code
  • +
+ ]]>
Zhongan Online @@ -20,7 +74,13 @@ com.intellij.modules.platform - com.intellij.modules.java + Git4Idea + com.intellij.modules.java + com.intellij.modules.python + com.intellij.modules.go-capable + com.intellij.modules.webstorm + + @@ -30,19 +90,30 @@ + + - + - + + + + + + + + messages.devpilot @@ -56,12 +127,27 @@ + + + + + + + + + + + + diff --git a/src/main/resources/html/login-fail.html b/src/main/resources/html/login-fail.html new file mode 100644 index 00000000..1d00c331 --- /dev/null +++ b/src/main/resources/html/login-fail.html @@ -0,0 +1,54 @@ + + + + + Login Failed - DevPilot + + + +
+ + + + + +

DevPilot

+

Login Failed

+

Go back to IntelliJ IDEA to login again

+
+ + + + \ No newline at end of file diff --git a/src/main/resources/html/login-success.html b/src/main/resources/html/login-success.html new file mode 100644 index 00000000..15bbe20a --- /dev/null +++ b/src/main/resources/html/login-success.html @@ -0,0 +1,53 @@ + + + + + Login Success - DevPilot + + + +
+ + + + +

DevPilot

+

Login Success

+

Go back to IntelliJ IDEA to experience DevPilot

+
+ + + + \ No newline at end of file diff --git a/src/main/resources/icons/account.svg b/src/main/resources/icons/account.svg new file mode 100644 index 00000000..bd785107 --- /dev/null +++ b/src/main/resources/icons/account.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/resources/icons/account_dark.svg b/src/main/resources/icons/account_dark.svg new file mode 100644 index 00000000..b1556792 --- /dev/null +++ b/src/main/resources/icons/account_dark.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/main/resources/icons/devpilot_13.svg b/src/main/resources/icons/devpilot_13.svg new file mode 100644 index 00000000..76f4c42d --- /dev/null +++ b/src/main/resources/icons/devpilot_13.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/devpilot_chat_shortcut.svg b/src/main/resources/icons/devpilot_chat_shortcut.svg new file mode 100644 index 00000000..76f4c42d --- /dev/null +++ b/src/main/resources/icons/devpilot_chat_shortcut.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/devpilot_disable.svg b/src/main/resources/icons/devpilot_disable.svg new file mode 100644 index 00000000..bf062a37 --- /dev/null +++ b/src/main/resources/icons/devpilot_disable.svg @@ -0,0 +1,17 @@ + + + openpilot- disable + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/devpilot_gray.svg b/src/main/resources/icons/devpilot_gray.svg new file mode 100644 index 00000000..33271b7d --- /dev/null +++ b/src/main/resources/icons/devpilot_gray.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/devpilot_gray_13.svg b/src/main/resources/icons/devpilot_gray_13.svg new file mode 100644 index 00000000..680ffba8 --- /dev/null +++ b/src/main/resources/icons/devpilot_gray_13.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/disconnect.svg b/src/main/resources/icons/disconnect.svg new file mode 100644 index 00000000..1fd91358 --- /dev/null +++ b/src/main/resources/icons/disconnect.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/disconnect_dark.svg b/src/main/resources/icons/disconnect_dark.svg new file mode 100644 index 00000000..5d99b371 --- /dev/null +++ b/src/main/resources/icons/disconnect_dark.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/login.svg b/src/main/resources/icons/login.svg new file mode 100644 index 00000000..c6cd3ce6 --- /dev/null +++ b/src/main/resources/icons/login.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/icons/login_dark.svg b/src/main/resources/icons/login_dark.svg new file mode 100644 index 00000000..b72df9dd --- /dev/null +++ b/src/main/resources/icons/login_dark.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/icons/logout.svg b/src/main/resources/icons/logout.svg new file mode 100644 index 00000000..bdec97a5 --- /dev/null +++ b/src/main/resources/icons/logout.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/icons/logout_dark.svg b/src/main/resources/icons/logout_dark.svg new file mode 100644 index 00000000..5b38ff0f --- /dev/null +++ b/src/main/resources/icons/logout_dark.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/icons/setting.svg b/src/main/resources/icons/setting.svg new file mode 100644 index 00000000..939ec29b --- /dev/null +++ b/src/main/resources/icons/setting.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/icons/setting_dark.svg b/src/main/resources/icons/setting_dark.svg new file mode 100644 index 00000000..34ca3384 --- /dev/null +++ b/src/main/resources/icons/setting_dark.svg @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/messages/devpilot_en.properties b/src/main/resources/messages/devpilot_en.properties index 11e52b03..7712eebe 100644 --- a/src/main/resources/messages/devpilot_en.properties +++ b/src/main/resources/messages/devpilot_en.properties @@ -3,6 +3,7 @@ devpilot.settings.service.modelHostLabel=Model Host devpilot.settings.service.modelTypeLabel=Choose Model devpilot.settings.service.modelNameLabel=Model Name devpilot.settings.service.customModelNameLabel=Custom Model Name +devpilot.settings.service.serviceTypeLabel=Choose Service devpilot.settings.service.apiKeyLabel=API Key devpilot.settings=DevPilot Settings notification.group.devpilot=DevPilot Notification Group @@ -17,16 +18,18 @@ devpilot.action.new.chat.desc=New chat with DevPilot devpilot.action.generate.comments=Generate Comments devpilot.action.generate.tests=Generate Tests devpilot.action.fix=Fix This -devpilot.action.review=Review Code +devpilot.action.review=Code Review devpilot.action.explain=Explain This devpilot.action.performance.check=Performance Check devpilot.action.changesview.generateCommit=Generate Commit Message +devpilot.action.edit.settings=Edit Settings... -devpilot.chatWindow.context.overflow=This model's maximum context length is 4096 tokens. +devpilot.chatWindow.context.overflow=This model's maximum context length is 16K tokens. devpilot.chatWindow.response.null=Nothing to see here. devpilot.alter.file.exist=File already exists. devpilot.alter.file.not.exist=File does not exist. +devpilot.alter.code.not.selected=Please select the code block first. devpilot.notification.input.tooLong=The input length is too long, please reduce the length of the messages or clear the conversation window. devpilot.notification.create.dir.failed=Failed to create directories. @@ -38,5 +41,39 @@ However, please keep in mind that there might be occasional surprises or mistake It's always a good idea to double-check any generated code or suggestions. devpilot.changesview.tokens.estimation.overflow=The estimated number of tokens exceeds the maximum limit, and unexpected results may occur. -devpilot.settings.service.refreshModelList=reload models -devpilot.settings.service.dialog.error=error \ No newline at end of file + +devpilot.settings.service.zaSsoLabel=User Login +devpilot.settings.service.zaSsoDesc=Login +devpilot.settings.service.zaWxLabel=Wechat Login +devpilot.settings.service.zaWxDesc=Login +devpilot.settings.service.logout=Logout +devpilot.settings.service.welcome=Hello + +devpilot.settings.service.code.completion.title=Code Auto Completion Configuration +devpilot.settings.service.code.completion.desc=Enable code auto completion +devpilot.settings.service.code.completion.enable.desc=Enable Code Auto Completion +devpilot.settings.service.code.completion.disabled.desc=Disabled Code Auto Completion + +devpilot.settings.service.chat.shortcut.disabled.desc=Disabled Method Shortcut +devpilot.settings.service.chat.shortcut.enable.desc=Enable Method Shortcut + +devpilot.settings.service.statusbar.login.desc=Login +devpilot.settings.service.statusbar.logout.desc=Logout + +devpilot.status.loggedIn=DevPilot +devpilot.status.notLoggedIn=DevPilot: Not login or login expired +devpilot.status.inCompletion=Fetching code completions +devpilot.status.account=Account: +devpilot.status.account.wx=WeChat* + +devpilot.inlay.shortcut.explain=Explain +devpilot.inlay.shortcut.fix=Fix +devpilot.inlay.shortcut.inlineComment=Comments +devpilot.inlay.shortcut.methodComments=Summary +devpilot.inlay.shortcut.test=Test + +devpilot.notification.update.message=An Update for DevPilot is available. We recommend to install the latest version! +devpilot.notification.installButton=install +devpilot.notification.hideButton=hide + +devpilot.error.report=Report to Devpilot \ No newline at end of file diff --git a/src/main/resources/messages/devpilot_zh.properties b/src/main/resources/messages/devpilot_zh.properties index 01f20294..8caa7535 100644 --- a/src/main/resources/messages/devpilot_zh.properties +++ b/src/main/resources/messages/devpilot_zh.properties @@ -1,35 +1,38 @@ devpilot.settings.service.title=\u670D\u52A1\u914D\u7F6E -devpilot.settings.service.modelHostLabel=\u6a21\u578b Host -devpilot.settings.service.modelTypeLabel=\u9009\u62e9\u6a21\u578b -devpilot.settings.service.modelNameLabel=\u6a21\u578b\u540d\u79f0 -devpilot.settings.service.customModelNameLabel=\u81ea\u5b9a\u4e49\u6a21\u578b\u540d -devpilot.settings.service.apiKeyLabel=API \u5bc6\u94a5 +devpilot.settings.service.modelHostLabel=\u6A21\u578B Host +devpilot.settings.service.modelTypeLabel=\u9009\u62E9\u6A21\u578B +devpilot.settings.service.modelNameLabel=\u6A21\u578B\u540D\u79F0 +devpilot.settings.service.customModelNameLabel=\u81EA\u5B9A\u4E49\u6A21\u578B\u540D +devpilot.settings.service.serviceTypeLabel=\u9009\u62E9\u670D\u52A1 +devpilot.settings.service.apiKeyLabel=API \u5BC6\u94A5 devpilot.settings=DevPilot\u8BBE\u7F6E notification.group.devpilot=DevPilot\u901A\u77E5\u7EC4 devpilot.setting.displayNameFieldLabel=\u7528\u6237\u540D devpilot.setting.language= \u8BED \u8A00 com.zhongan.devpilot.actions.editor.popupmenu.BasicEditorAction=DevPilot devpilot.prompt.text.placeholder=\u8BF7\u8F93\u5165\u60A8\u7684\u95EE\u9898 -devpilot.reference.content=\u5f15\u7528\uff1a +devpilot.reference.content=\u5F15\u7528\uFF1A devpilot.action.new.chat=\u65B0\u5EFADevPilot\u4F1A\u8BDD devpilot.action.new.chat.desc=\u6253\u5F00\u65B0\u7684DevPilot\u4F1A\u8BDD -devpilot.action.generate.comments=\u751F\u6210\u6CE8\u91CA -devpilot.action.generate.tests=\u751F\u6210\u6D4B\u8BD5 -devpilot.action.fix=\u4ee3\u7801\u4FEE\u590D +devpilot.action.generate.comments=\u884C\u5185\u6CE8\u91CA +devpilot.action.generate.tests=\u751F\u6210\u5355\u6D4B +devpilot.action.fix=\u4FEE\u590D\u4EE3\u7801 devpilot.action.review=\u4EE3\u7801\u5BA1\u67E5 -devpilot.action.explain=\u89E3\u91CA\u4ee3\u7801 +devpilot.action.explain=\u89E3\u91CA\u4EE3\u7801 devpilot.action.performance.check=\u6027\u80FD\u68C0\u67E5 -devpilot.action.changesview.generateCommit=\u751f\u6210\u63d0\u4ea4\u65e5\u5fd7 +devpilot.action.changesview.generateCommit=\u751F\u6210\u63D0\u4EA4\u65E5\u5FD7 +devpilot.action.edit.settings=\u7F16\u8F91\u8BBE\u7F6E -devpilot.chatWindow.context.overflow=\u6a21\u578b\u4e0a\u4e0b\u6587\u6700\u5927\u4e3a4096 tokens. -devpilot.chatWindow.response.null=\u65e0\u54cd\u5e94\uff0c\u8bf7\u91cd\u8bd5 +devpilot.chatWindow.context.overflow=\u6A21\u578B\u4E0A\u4E0B\u6587\u6700\u5927\u4E3A 16k tokens. +devpilot.chatWindow.response.null=\u65E0\u54CD\u5E94\uFF0C\u8BF7\u91CD\u8BD5 -devpilot.alter.file.exist=\u6587\u4ef6\u5df2\u5b58\u5728 -devpilot.alter.file.not.exist=\u6587\u4ef6\u4e0d\u5b58\u5728 +devpilot.alter.file.exist=\u6587\u4EF6\u5DF2\u5B58\u5728 +devpilot.alter.file.not.exist=\u6587\u4EF6\u4E0D\u5B58\u5728 +devpilot.alter.code.not.selected=\u8BF7\u5148\u9009\u62E9\u4EE3\u7801\u5757 -devpilot.notification.input.tooLong=\u8bf7\u51cf\u5c11\u63d0\u4f9b\u7684\u4e0a\u4e0b\u6587\u6216\u6e05\u9664\u4f1a\u8bdd\u8bb0\u5f55 -devpilot.notification.create.dir.failed=\u521b\u5efa\u6587\u4ef6\u5939\u5931\u8d25 +devpilot.notification.input.tooLong=\u8BF7\u51CF\u5C11\u63D0\u4F9B\u7684\u4E0A\u4E0B\u6587\u6216\u6E05\u9664\u4F1A\u8BDD\u8BB0\u5F55 +devpilot.notification.create.dir.failed=\u521B\u5EFA\u6587\u4EF6\u5939\u5931\u8D25 devpilot.welcome.assist=\u60A8\u597D @%s\uFF0C \u6211\u6709\u4EC0\u4E48\u53EF\u4EE5\u5E2E\u52A9\u60A8\u5417\uFF1F\

\ @@ -37,7 +40,39 @@ devpilot.welcome.assist=\u60A8\u597D @%s - - - - - DevPilot - - - - -
- - + + + +
+ + + + + \ No newline at end of file diff --git a/src/test/java/com/zhongan/devpilot/util/JsonUtilsTest.java b/src/test/java/com/zhongan/devpilot/util/JsonUtilsTest.java deleted file mode 100644 index c9a76913..00000000 --- a/src/test/java/com/zhongan/devpilot/util/JsonUtilsTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.zhongan.devpilot.util; - -import com.zhongan.devpilot.integrations.llms.entity.DevPilotSuccessStreamingResponse; - -import org.junit.Assert; -import org.junit.Test; - -public class JsonUtilsTest { - - @Test - public void fromJson() { - String data = "{\"id\":\"chatcmpl-96x1WN8Eo9zJCjrvZxBh1VBDodPsO\",\"object\":\"chat.completion.chunk\",\"created\":1711443882,\"model\":\"gpt-3.5-turbo-0125\",\"system_fingerprint\":\"fp_3bc1b5746c\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"logprobs\":null,\"finish_reason\":null}]}"; - DevPilotSuccessStreamingResponse resp = JsonUtils.fromJson(data, DevPilotSuccessStreamingResponse.class); - Assert.assertNotNull(resp); - Assert.assertEquals(resp.getId(),"chatcmpl-96x1WN8Eo9zJCjrvZxBh1VBDodPsO"); - } -}