Skip to content
This repository has been archived by the owner on Jul 27, 2024. It is now read-only.

Introduce the ObjectAttributeCompletionProvider module #654

Merged
merged 14 commits into from
Nov 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/theme_check/language_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
require_relative "language_server/versioned_in_memory_storage"
require_relative "language_server/client_capabilities"

require_relative "language_server/completion_context"
require_relative "language_server/completion_helper"
require_relative "language_server/completion_provider"
require_relative "language_server/completion_engine"
Expand Down
44 changes: 44 additions & 0 deletions lib/theme_check/language_server/completion_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module ThemeCheck
module LanguageServer
class CompletionContext
include PositionHelper

attr_reader :storage, :relative_path, :line, :col

def initialize(storage, relative_path, line, col)
@storage = storage
@relative_path = relative_path
@line = line
@col = col
end

def buffer
@buffer ||= storage.read(relative_path)
end

def absolute_cursor
@absolute_cursor ||= from_row_column_to_index(buffer, line, col)
end

def cursor
@cursor ||= absolute_cursor - token&.start || 0
end

def content
@content ||= token&.content
end

def token
@token ||= Tokens.new(buffer).find do |t|
# Here we include the next character and exclude the first
# one becase when we want to autocomplete inside a token
# and at most 1 outside it since the cursor could be placed
# at the end of the token.
t.start < absolute_cursor && absolute_cursor <= t.end
end
end
end
end
end
6 changes: 5 additions & 1 deletion lib/theme_check/language_server/completion_engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def initialize(storage, bridge = nil)

def completions(relative_path, line, col)
@providers.flat_map do |provider|
provider.completions(relative_path, line, col)
provider.completions(context(relative_path, line, col))
end
rescue StandardError => error
@bridge || raise(error)
Expand All @@ -21,6 +21,10 @@ def completions(relative_path, line, col)

@bridge.log("[completion error] error: #{message}\n#{backtrace}")
end

def context(relative_path, line, col)
CompletionContext.new(@storage, relative_path, line, col)
end
end
end
end
28 changes: 0 additions & 28 deletions lib/theme_check/language_server/completion_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ module ThemeCheck
module LanguageServer
class CompletionProvider
include CompletionHelper
include PositionHelper
include RegexHelpers

attr_reader :storage
Expand All @@ -29,33 +28,6 @@ def completions(relative_path, line, col)
raise NotImplementedError
end

def current_token(relative_path, line, col)
buffer = read_file(relative_path)
absolute_cursor = from_row_column_to_index(buffer, line, col)
token = find_token(buffer, absolute_cursor)

return [] if token.nil?

content = token.content
cursor = absolute_cursor - token.start

CurrentToken.new(content, cursor, absolute_cursor, buffer)
end

def read_file(relative_path)
storage.read(relative_path)
end

def find_token(buffer, cursor)
Tokens.new(buffer).find do |token|
# Here we include the next character and exclude the first
# one becase when we want to autocomplete inside a token
# and at most 1 outside it since the cursor could be placed
# at the end of the token.
token.start < cursor && cursor <= token.end
end
end

def doc_hash(content)
return {} if content.nil? || content.empty?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ module LanguageServer
class FilterCompletionProvider < CompletionProvider
NAMED_FILTER = /#{Liquid::FilterSeparator}\s*(\w+)/o

def completions(relative_path, line, col)
token = current_token(relative_path, line, col)
content = token.content
cursor = token.cursor
def completions(context)
content = context.content
cursor = context.cursor

return [] if content.nil?
return [] unless can_complete?(content, cursor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
module ThemeCheck
module LanguageServer
class ObjectAttributeCompletionProvider < CompletionProvider
def completions(relative_path, line, col)
token = current_token(relative_path, line, col)

return [] if token.content.nil?
return [] unless (variable_lookup = VariableLookupFinder.lookup(token))
def completions(context)
return [] if context.content.nil?
return [] unless (variable_lookup = VariableLookupFinder.lookup(context))

# Navigate through lookups until the last valid [object, property] level
object, property = lookup_object_and_property(variable_lookup)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
module ThemeCheck
module LanguageServer
class ObjectCompletionProvider < CompletionProvider
def completions(relative_path, line, col)
token = current_token(relative_path, line, col)
def completions(context)
content = context.content

return [] if token.content.nil?
return [] unless (variable_lookup = VariableLookupFinder.lookup(token))
return [] if content.nil?
return [] unless (variable_lookup = VariableLookupFinder.lookup(context))
return [] unless variable_lookup.lookups.empty?
return [] if token.content[token.cursor - 1] == "."
return [] if content[context.cursor - 1] == "."

ShopifyLiquid::Object.labels
.select { |w| w.start_with?(partial(variable_lookup)) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
module ThemeCheck
module LanguageServer
class RenderSnippetCompletionProvider < CompletionProvider
def completions(relative_path, line, col)
token = current_token(relative_path, line, col)
content = token.content
cursor = token.cursor
def completions(context)
content = context.content
cursor = context.cursor

return [] if content.nil?
return [] unless cursor_on_quoted_argument?(content, cursor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
module ThemeCheck
module LanguageServer
class TagCompletionProvider < CompletionProvider
def completions(relative_path, line, col)
token = current_token(relative_path, line, col)
content = token.content
cursor = token.cursor
def completions(context)
content = context.content
cursor = context.cursor

return [] if content.nil?
return [] unless can_complete?(content, cursor)
Expand Down
14 changes: 7 additions & 7 deletions lib/theme_check/language_server/variable_lookup_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@ module VariableLookupFinder

PotentialLookup = Struct.new(:name, :lookups)

def lookup(token)
content = token.content
cursor = token.cursor
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_tag(content, cursor)

# And we only return it if it's parsed by Liquid as VariableLookup
return unless variable_lookup.is_a?(Liquid::VariableLookup)

potential_lookup(variable_lookup, token)
potential_lookup(variable_lookup, context)
end

private

def potential_lookup(variable, token)
return variable if token.buffer.nil? || token.buffer.empty?
def potential_lookup(variable, context)
return variable if context.buffer.nil? || context.buffer.empty?

buffer = token.buffer[0...token.absolute_cursor]
buffer = context.buffer[0...context.absolute_cursor]
lookups = variable.lookups
assignments = find_assignments(buffer)

Expand Down
63 changes: 63 additions & 0 deletions test/language_server/completion_context_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_string_literal: true
require "test_helper"

module ThemeCheck
module LanguageServer
class CompletionContextTest < Minitest::Test
def setup
super
skip("Liquid-C not supported") if liquid_c_enabled?
end

def test_buffer
assert_equal(buffer, context.buffer)
end

def test_token
assert_can_find_token("{% rend %}")
assert_can_find_token("{{ 'foo.js' | }}")
assert_can_find_token("<head>\n ")
assert_can_find_token("\"></script>\n ")
assert_can_find_token("{% rend\n</head>\n")
end

private

def context(line: 0, col: 0, absolute_cursor: nil)
ctx = CompletionContext.new(storage, file_name, line, col)
ctx.stubs(absolute_cursor: absolute_cursor) unless absolute_cursor.nil?
ctx
end

def storage
InMemoryStorage.new(file_name => buffer)
end

def file_name
"layout/theme.liquid"
end

def buffer
<<~LIQUID
<head>
{% rend %}
<script src="{{ 'foo.js' | }}"></script>
{% rend
</head>
LIQUID
end

def assert_can_find_token(token)
# Being on the first character of a token should try to
# complete the previous one
refute_equal(token, context(absolute_cursor: buffer.index(token)).token&.content)

# Being inside the token should give you the token
assert_equal(token, context(absolute_cursor: buffer.index(token) + 1).token.content)

# Being on the next character (outside the token) should give you the previous one.
assert_equal(token, context(absolute_cursor: buffer.index(token) + token.size).token.content)
end
end
end
end
36 changes: 3 additions & 33 deletions test/language_server/completion_provider_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,6 @@ def setup
skip("Liquid-C not supported") if liquid_c_enabled?
end

def test_find_token
content = <<~LIQUID
<head>
{% rend %}
<script src="{{ 'foo.js' | }}"></script>
{% rend
</head>
LIQUID
provider = make_provider(filename => content)

assert_can_find_token(provider, content, "{% rend %}")
assert_can_find_token(provider, content, "{{ 'foo.js' | }}")
assert_can_find_token(provider, content, "<head>\n ")
assert_can_find_token(provider, content, "\"></script>\n ")
assert_can_find_token(provider, content, "{% rend\n</head>\n")
end

def test_doc_hash
expected_hash = {
documentation: {
Expand All @@ -45,25 +28,12 @@ def test_doc_hash_with_empty_content

private

def make_provider(files = {})
storage = InMemoryStorage.new(files)
def make_provider
CompletionProvider.new(storage)
end

def filename
"layout/theme.liquid"
end

def assert_can_find_token(provider, content, token)
# Being on the first character of a token should try to
# complete the previous one
refute_equal(token, provider.find_token(content, content.index(token))&.content)

# Being inside the token should give you the token
assert_equal(token, provider.find_token(content, content.index(token) + 1).content)

# Being on the next character (outside the token) should give you the previous one.
assert_equal(token, provider.find_token(content, content.index(token) + token.size).content)
def storage
InMemoryStorage.new
end
end
end
Expand Down
Loading