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
27 changes: 27 additions & 0 deletions spec/reproduction_41_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require "spec"
require "../src/crystalline/text_document"
require "lsp/base/range"

describe Crystalline::TextDocument do
it "fixes #41: does not strip newlines during incremental updates" do
initial_content = "def foo\nend\n"
doc = Crystalline::TextDocument.new(URI.parse("file:///test.cr"), nil, initial_content)
doc.contents.should eq(initial_content)

# Simulation: Append a comment at the end of the line (at the newline position)
# Line 0 is "def foo\n" (length 8)
# Character 8 is the \n.
range = LSP::Range.new(
start: LSP::Position.new(line: 0, character: 8),
end: LSP::Position.new(line: 0, character: 8)
)

# If .chomp was used, prefix would be "def foo" (stripping \n).
# Adding "# comment" would result in "def foo# commentend\n" (LOST newline).
# WITHOUT .chomp, prefix is "def foo\n".
# Result should be "def foo\n# commentend\n"

doc.update_contents([{"# comment", range}], version: 2)
doc.contents.should eq("def foo\n# commentend\n")
end
end
51 changes: 51 additions & 0 deletions spec/reproduction_63_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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 #63: range formatting does not add extra newlines" do
server = FakeServer.new
workspace = Crystalline::Workspace.new(server, "file:///tmp")

file_uri = URI.parse("file:///tmp/test_paste.cr")
initial_content = "foo \"bar\"\n"

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

# Range is the whole first line except the newline.
range = LSP::Range.new(
start: LSP::Position.new(line: 0, character: 0),
end: LSP::Position.new(line: 0, character: 9)
)

params = LSP::DocumentRangeFormattingParams.new(
text_document: LSP::TextDocumentIdentifier.new(uri: file_uri.to_s),
range: range,
options: LSP::FormattingOptions.new(tab_size: 2, insert_spaces: true)
)

result = workspace.format_document(params)
result.should_not be_nil
if result
formatted_text, _ = result
# Crystal.format("foo \"bar\"") normally returns "foo \"bar\"\n"
# Our fix should chomp it.
formatted_text.should eq("foo \"bar\"")
end
end
end
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
38 changes: 34 additions & 4 deletions src/crystalline.cr
Original file line number Diff line number Diff line change
@@ -1,9 +1,39 @@
require "./crystalline/requires"
require "./crystalline/*"
require "option_parser"

if ARGV.includes?("--version")
puts(Crystalline::VERSION)
exit
log_level = ::Log::Severity::Warn

OptionParser.parse do |parser|
parser.banner = "Usage: crystalline [options]"

parser.on("-v", "--version", "Show version") do
puts Crystalline::VERSION
exit
end

parser.on("-h", "--help", "Show help") do
puts parser
exit
end

parser.on("-l LEVEL", "--log LEVEL", "Set log level (debug, info, warn, error). Default: warn") do |level|
log_level = case level.downcase
when "debug" then ::Log::Severity::Debug
when "info" then ::Log::Severity::Info
when "warn" then ::Log::Severity::Warn
when "error" then ::Log::Severity::Error
else
STDERR.puts "Invalid log level: #{level}"
exit 1
end
end

parser.invalid_option do |flag|
STDERR.puts "ERROR: #{flag} is not a valid option."
STDERR.puts parser
exit 1
end
end

Crystalline.init
Crystalline.init(log_level: log_level)
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
5 changes: 3 additions & 2 deletions src/crystalline/ext/boehm.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module GC
def self.init
# LibGC.set_free_space_divisor(10)
# LibGC.set_force_unmap_on_gcollect(1)
LibGC.set_free_space_divisor(10)
LibGC.set_force_unmap_on_gcollect(1)
LibGC.enable_incremental
previous_def
end
end
Expand Down
15 changes: 11 additions & 4 deletions src/crystalline/main.cr
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,19 @@ module Crystalline
end
end

def self.init(*, input : IO = STDIN, output : IO = STDOUT)
def self.init(*, input : IO = STDIN, output : IO = STDOUT, log_level : ::Log::Severity = :warn)
# Setup a temporary backend to STDERR so we can log early initialization errors.
# This MUST be done first to avoid STDOUT corruption.
::Log.setup(log_level, ::Log::IOBackend.new(STDERR))

EnvironmentConfig.run
{% if flag?(:debug) %}
::Log.setup(:debug, LSP::Log.backend.not_nil!)
{% end %}
server = LSP::Server.new(input, output, SERVER_CAPABILITIES)

# Re-setup with the LSP backend once the server is initialized.
if (backend = LSP::Log.backend)
::Log.setup(log_level, backend)
end

Controller.new(server)
rescue ex
LSP::Log.error(exception: ex) { %(#{ex.message || "Unknown error during init."}\n#{ex.backtrace.join('\n')}) }
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
4 changes: 2 additions & 2 deletions src/crystalline/result_cache.cr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
class Crystalline::ResultCache
# A monotonic timestamp used to store the invalidation date.
@@reference_clock = Time.monotonic
@@reference_clock = Time.instant
# A cache of compiler results with invalidation time, indexed by file name.
@cache : Hash(String, {Crystal::Compiler::Result?, Time::Span?}) = Hash(String, {Crystal::Compiler::Result?, Time::Span?}).new

Expand Down Expand Up @@ -42,6 +42,6 @@ class Crystalline::ResultCache

# Return the current monotonic time.
def monotonic_now
Time.monotonic - @@reference_clock
Time.instant - @@reference_clock
end
end
2 changes: 1 addition & 1 deletion src/crystalline/text_document.cr
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class Crystalline::TextDocument
end

private def partial_update(contents : String, range : LSP::Range, version : Number? = nil)
prefix = @inner_contents[range.start.line]?.try &.[...range.start.character].chomp || ""
prefix = @inner_contents[range.start.line]?.try &.[...range.start.character] || ""
suffix = @inner_contents[range.end.line]?.try &.[range.end.character..]? || @inner_contents[range.end.line]? || ""
replacement_lines = String.build { |str|
str << prefix << contents << suffix
Expand Down
Loading