Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
59 changes: 59 additions & 0 deletions spec/reproduction_76_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
require "spec"
require "lsp/server"
require "../src/crystalline/requires"
require "../src/crystalline/*"

class FakeServer < LSP::Server
def initialize
super(IO::Memory.new, IO::Memory.new)
@client_capabilities = LSP::ClientCapabilities.new
end
end

describe Crystalline::Workspace do
it "fixes #76: does not trigger completion inside comments" do
server = FakeServer.new
workspace = Crystalline::Workspace.new(server, "file:///tmp")

file_uri = URI.parse("file:///tmp/test_comment.cr")
# A file with a dot inside a comment
content = <<-CRYSTAL
# This is a comment.
puts 1
CRYSTAL

workspace.open_document(LSP::DidOpenTextDocumentParams.new(
text_document: LSP::TextDocumentItem.new(
uri: file_uri.to_s,
language_id: "crystal",
version: 1,
text: content
)
))

# Try to trigger completion at the dot in the comment
# Line 0, character 19 (the dot at the end of "comment.")
pos = LSP::Position.new(line: 0, character: 19)

result = workspace.completion(server, file_uri, pos, ".")

# It should return nil early because it's in a comment.
result.should be_nil

# Test case: # inside a string should NOT be a comment
content_with_string = <<-CRYSTAL
x = "this is # not a comment."
CRYSTAL
workspace.update_document(server, LSP::DidChangeTextDocumentParams.new(
text_document: LSP::VersionedTextDocumentIdentifier.new(uri: file_uri.to_s, version: 2),
content_changes: [
LSP::DidChangeTextDocumentParams::TextDocumentContentChangeEvent.new(
text: content_with_string
),
]
))
# Dot at end of string. Line 0, character 31.
pos_in_string = LSP::Position.new(line: 0, character: 31)
workspace.completion(server, file_uri, pos_in_string, ".")
end
end
16 changes: 11 additions & 5 deletions src/crystalline/analysis/analysis.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ module Crystalline::Analysis
end
{% end %}

private def self.spawn_dedicated(*, name : String? = nil, &block)
fiber = Fiber.new(name, &block)
{% if flag?(:preview_mt) %} fiber.set_current_thread(@@dedicated_thread) {% end %}
fiber.enqueue
fiber
private def self.spawn_dedicated(*, name : String? = nil, &block : -> _)
captured_block = block
{% if flag?(:preview_mt) %}
fiber = Fiber.new(name, &captured_block)
fiber.set_current_thread(@@dedicated_thread)
fiber.enqueue
fiber
{% else %}
captured_block.call
Fiber.current
{% end %}
end

# Compile a target *file_uri*.
Expand Down
30 changes: 20 additions & 10 deletions src/crystalline/progress.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,34 @@ class Crystalline::Progress
end

def report(server, *, async = false, &cb : Proc(String?))
create_request = LSP::WorkDoneProgressCreateRequest.new(
id: 0,
params: LSP::WorkDoneProgressCreateParams.new(
token: @token,
),
)
if server.client_capabilities.window.try &.work_done_progress
create_request = LSP::WorkDoneProgressCreateRequest.new(
id: 0,
params: LSP::WorkDoneProgressCreateParams.new(
token: @token,
),
)

create_request.on_response {
create_request.on_response {
if async
spawn {
report_callback(server, &cb)
}
else
report_callback(server, &cb)
end
}

server.send(create_request)
else
if async
spawn {
report_callback(server, &cb)
}
else
report_callback(server, &cb)
end
}

server.send(create_request)
end
end

def send_progress_start(server)
Expand Down
62 changes: 49 additions & 13 deletions src/crystalline/workspace.cr
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class Crystalline::Workspace
return cached_result unless cached_result.nil? && discard_nil_cached_result
end

sync_channel = Channel(Crystal::Compiler::Result?).new
sync_channel = Channel(Crystal::Compiler::Result?).new(1)

progress.report(server) do
file_overrides = nil
Expand All @@ -172,16 +172,13 @@ class Crystalline::Workspace
@opened_documents.each { |uri_str, text_document|
contents = text_overrides.try(&.[uri_str]?) || text_document.contents
contents = fix_source(contents)

if target_string == uri_str
# If the entry point itself needs to be loaded from memory.
sources = [
Crystal::Compiler::Source.new(target.decoded_path, contents),
]
end
file_path = URI.parse(uri_str).decoded_path
file_overrides[file_path] = contents
file_overrides[URI.parse(uri_str).decoded_path] = contents
}

if (doc = @opened_documents[target_string]?)
contents = text_overrides.try(&.[target_string]?) || doc.contents
sources = [Crystal::Compiler::Source.new(target.decoded_path, fix_source(contents))]
end
end

lib_path = project.try(&.default_lib_path)
Expand Down Expand Up @@ -361,11 +358,51 @@ class Crystalline::Workspace
nil
end

def completion(server : LSP::Server, file_uri : URI, position : LSP::Position, trigger_character : String?)
def completion(server : LSP::Server, file_uri : URI, position : LSP::Position, trigger_character : String?) : LSP::CompletionList?
text_document = @opened_documents[file_uri.to_s]?
return unless text_document

# LSP::Log.info { "completion: #{trigger_character}"}
# Check if we are inside a comment.
current_line = text_document.contents.lines(chomp: false)[position.line]?
if current_line
# Convert UTF-16 character position to byte offset for the lexer.
byte_offset = 0
char_offset = 0
current_line.each_char do |char|
# In UTF-16, characters > 0xFFFF take 2 code units (surrogate pair).
u16_size = char.ord > 0xFFFF ? 2 : 1
break if char_offset + u16_size > position.character

byte_offset += char.bytesize
char_offset += u16_size
end
# Lexer column numbers are 1-based byte offsets.
lexer_column = byte_offset + 1

lexer = Crystal::Lexer.new(current_line)
begin
loop do
token = lexer.next_token
break if token.type.eof?

if (loc = token.location)
break if loc.column_number > lexer_column
end

if token.type.comment?
if (loc = token.location)
token_start = loc.column_number
# If the cursor is at or after the start of the comment.
if lexer_column >= token_start
return nil
end
end
end
end
rescue Crystal::SyntaxException
# Ignore syntax errors while typing
end
end

document_lines = fix_source(text_document.contents).lines(chomp: false)
left_offset = 0
Expand Down Expand Up @@ -420,7 +457,6 @@ class Crystalline::Workspace
result = self.compile(
server,
file_uri,
in_memory: true,
discard_nil_cached_result: true,
wants_doc: true,
text_overrides: text_overrides,
Expand Down