# frozen_string_literal: true require 'ostruct' module PlatformosCheck module LanguageServer module VariableLookupFinder include Constants include TypeHelper extend self def lookup(context) content = context.content cursor = context.cursor return if cursor_is_on_bracket_position_that_cant_be_completed(content, cursor) variable_lookup = lookup_liquid_variable(content, cursor) || lookup_liquid_variable_inside_liquid_tag(content, cursor) || lookup_liquid_tag(content, cursor) return variable_lookup if variable_lookup.is_a?(PotentialLookup) return unless variable_lookup.is_a?(Liquid::VariableLookup) potential_lookup(variable_lookup, context) end def lookup_literal(context) lookup_liquid_variable(context.content, context.cursor) end private def potential_lookup(variable, context) return as_potential_lookup(variable) if context.buffer.nil? || context.buffer.empty? buffer = context.buffer[0...context.absolute_cursor] lookups = variable.lookups assignments = find_assignments(buffer) assignments_path = [] while assignments[variable.name] && !assignments_path.include?(assignments[variable.name]) variable = assignments[variable.name] lookups = variable.lookups + lookups assignments_path << variable end as_potential_lookup(variable, lookups:) end def find_assignments(buffer) finder = AssignmentsFinder.new(buffer) finder.find! finder.assignments end def as_potential_lookup(variable, lookups: nil) PotentialLookup.new( variable.name, lookups || variable.lookups, nil, variable.respond_to?(:file_path) ? variable.file_path : nil ) end def cursor_is_on_bracket_position_that_cant_be_completed(content, cursor) content_before_cursor = content[0..cursor - 1] return false unless /[\[\]]/.match?(content_before_cursor) line_with_cursor = content_before_cursor.split("\n").last line_with_cursor =~ ENDS_IN_BRACKET_POSITION_THAT_CANT_BE_COMPLETED end def cursor_is_on_liquid_variable_lookup_position(content, cursor) previous_char = content[cursor - 1] is_liquid_variable = content =~ Liquid::VariableStart is_in_variable_segment = previous_char =~ VARIABLE_LOOKUP_CHARACTERS is_on_blank_variable_lookup_position = content[0..cursor - 1] =~ /[{:,-]\s+$/ ( is_liquid_variable && ( is_in_variable_segment || is_on_blank_variable_lookup_position ) ) end def lookup_liquid_variable_inside_liquid_tag(content, cursor) return unless content.strip.start_with?(/{%\s*liquid/) previous_char = content[cursor - 1] is_in_variable_segment = previous_char =~ VARIABLE_LOOKUP_CHARACTERS return unless is_in_variable_segment start_index = content.slice(0, cursor).rindex("\n") + 1 end_index = cursor - 1 # We take the following content # - start after the first two {{ # - end at cursor position # # That way, we'll have a partial liquid variable that # can be parsed such that the "last" variable_lookup # will be the one we're trying to complete. markup = content[start_index..end_index] # Early return for incomplete variables return empty_lookup if /\s+$/.match?(markup) # Now we go to hack city... The cursor might be in the middle # of a string/square bracket lookup. We need to close those # otherwise the variable parse won't work. markup += "'" if markup.count("'").odd? markup += '"' if markup.count('"').odd? markup += "]" if UNCLOSED_SQUARE_BRACKET.match?(markup) if markup.strip.split(' ').size > 1 begin template = LiquidFile.parse(parseable_markup(content, cursor)) current_tag = template.root.nodelist[0] return if current_tag.is_a?(Liquid::Tag) rescue Liquid::SyntaxError return end end variable = variable_from_markup(markup) variable_lookup_for_liquid_variable(variable) end def lookup_liquid_variable(content, cursor) return unless cursor_is_on_liquid_variable_lookup_position(content, cursor) start_index = content.match(/#{Liquid::VariableStart}-?/o).end(0) + 1 end_index = cursor - 1 # We take the following content # - start after the first two {{ # - end at cursor position # # That way, we'll have a partial liquid variable that # can be parsed such that the "last" variable_lookup # will be the one we're trying to complete. markup = content[start_index..end_index] # Early return for incomplete variables return empty_lookup if /\s+$/.match?(markup) # Now we go to hack city... The cursor might be in the middle # of a string/square bracket lookup. We need to close those # otherwise the variable parse won't work. markup += "'" if markup.count("'").odd? markup += '"' if markup.count('"').odd? markup += "]" if UNCLOSED_SQUARE_BRACKET.match?(markup) variable = variable_from_markup(markup) variable_lookup_for_liquid_variable(variable) end def cursor_is_on_liquid_tag_lookup_position(content, cursor) markup = content[0..cursor - 1] is_liquid_tag = content.match?(Liquid::TagStart) is_in_variable_segment = markup =~ ENDS_WITH_POTENTIAL_LOOKUP is_on_blank_variable_lookup_position = markup =~ ENDS_WITH_BLANK_POTENTIAL_LOOKUP ( is_liquid_tag && ( is_in_variable_segment || is_on_blank_variable_lookup_position ) ) end # Context: # # We know full well that the code as it is being typed is probably not # something that can be parsed by liquid. # # How this works: # # 1. Attempt to turn the code of the token until the cursor position into # valid liquid code with some hacks. # 2. If the code ends in space at a "potential lookup" spot # a. Then return an empty variable lookup # 3. Parse the valid liquid code # 4. Attempt to extract a VariableLookup from Liquid::Template def lookup_liquid_tag(content, cursor) return unless cursor_is_on_liquid_tag_lookup_position(content, cursor) markup = parseable_markup(content, cursor) return empty_lookup if markup.empty? template = LiquidFile.parse(markup) current_tag = template.root.nodelist[0] case current_tag&.tag_name when "if", "unless" variable_lookup_for_if_tag(current_tag) when "case" variable_lookup_for_case_tag(current_tag) when "cycle" variable_lookup_for_cycle_tag(current_tag) when "for" variable_lookup_for_for_tag(current_tag) when "tablerow" variable_lookup_for_tablerow_tag(current_tag) when "render" variable_lookup_for_render_tag(current_tag) when "assign" variable_lookup_for_assign_tag(current_tag) when "echo" variable_lookup_for_echo_tag(current_tag) when "function" variable_lookup_for_function_tag(current_tag) when "return" variable_lookup_for_return_tag(current_tag) when "log" variable_lookup_for_log_tag(current_tag) else empty_lookup end rescue Liquid::SyntaxError # We don't complete variable for liquid syntax errors empty_lookup end def parseable_markup(content, cursor = nil) LiquidFixer.new(content, cursor).parsable end def variable_lookup_for_if_tag(if_tag) condition = if_tag.blocks.last variable_lookup_for_condition(condition) end def variable_lookup_for_condition(condition) return variable_lookup_for_condition(condition.child_condition) if condition.child_condition return condition.right if condition.right condition.left end def variable_lookup_for_case_tag(case_tag) return variable_lookup_for_case_block(case_tag.blocks.last) unless case_tag.blocks.empty? case_tag.left end def variable_lookup_for_case_block(condition) condition.right end def variable_lookup_for_cycle_tag(cycle_tag) cycle_tag.variables.last end def variable_lookup_for_for_tag(for_tag) for_tag.collection_name end def variable_lookup_for_tablerow_tag(tablerow_tag) tablerow_tag.collection_name end def variable_lookup_for_render_tag(render_tag) return empty_lookup if /:\s*$/.match?(render_tag.raw) render_tag.attributes.values.last end def variable_lookup_for_assign_tag(assign_tag) variable_lookup_for_liquid_variable(assign_tag.from) end def variable_lookup_for_echo_tag(echo_tag) variable_lookup_for_liquid_variable(echo_tag.variable) end def variable_lookup_for_function_tag(function_tag) return empty_lookup if /:\s*$/.match?(function_tag.raw) function_tag.attributes.values.last end def variable_lookup_for_return_tag(return_tag) return_tag.variable end def variable_lookup_for_log_tag(log_tag) log_tag.variable end def variable_lookup_for_liquid_variable(variable) has_filters = !variable.filters.empty? # Can complete after trailing comma or : if has_filters && variable.raw =~ /[:,]\s*$/ empty_lookup elsif has_filters last_filter_argument(variable.filters) elsif variable.name.nil? empty_lookup elsif variable.name.is_a?(Liquid::VariableLookup) variable.name else PotentialLookup.new(input_type_of(variable.name), []) end end def empty_lookup Liquid::VariableLookup.parse('') end # We want the last thing in variable.filters which is at most # an array that looks like [name, positional_args, hash_arg] def last_filter_argument(filters) filter = filters.last return filter[2].values.last if filter.size == 3 return filter[1].last if filter.size == 2 nil end def variable_from_markup(markup, parse_context = Liquid::ParseContext.new) Liquid::Variable.new(markup, parse_context) end def tag_regex(tag) PlatformosLiquid::Tag.tag_regex(tag) end end end end