Skip to content

Commit 699cc79

Browse files
authored
Feature/socket support (#101)
* Cruft * Add support for using sockets using LSP_PORT to set it. * Tweak io a little. Add some tests. * Add telnet for testing * Remove another stray io reference * Add run_in_shell * Convert io_spec * cops * sqlite plugin sometimes not loaded (#102) Fixes #99 * Downgrade to ruby 3.3. * Clear some warnings.
1 parent 9a4ca37 commit 699cc79

6 files changed

Lines changed: 207 additions & 82 deletions

File tree

Makefile

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.PHONY: image guard continuous_development console test shell run_in_shell server gem gem_release publish_cross_platform_image
12
PROJECT_NAME=ruby_language_server
23
LOCAL_LINK=-v $(PWD):/tmp/src -w /tmp/src
34

@@ -9,7 +10,7 @@ force_rebuild_image:
910

1011
guard: image
1112
echo > active_record.log
12-
docker run -it --rm $(LOCAL_LINK) -e LOG_LEVEL=DEBUG $(PROJECT_NAME) bundle exec guard
13+
./bin/run_in_shell bundle exec guard
1314
echo > active_record.log
1415

1516
continuous_development: image
@@ -23,30 +24,30 @@ continuous_development: image
2324
done
2425

2526
console: image
26-
docker run -it --rm $(LOCAL_LINK) $(PROJECT_NAME) bin/console
27+
./bin/run_in_shell bin/console
2728

2829
test: image
29-
docker run --rm $(LOCAL_LINK) $(PROJECT_NAME) sh -c 'bundle exec rake test && bundle exec rubocop'
30+
./bin/run_in_shell "bundle exec rake test && bundle exec rubocop"
3031

3132
shell: image
32-
docker run -it --rm $(LOCAL_LINK) $(PROJECT_NAME) sh
33+
./bin/run_in_shell sh
3334

34-
run_in_shell: image
35-
docker run -it --rm $(LOCAL_LINK) $(PROJECT_NAME) sh -c '$(COMMAND)'
35+
run_in_shell:
36+
docker run -it --rm $(LOCAL_LINK) $(PROJECT_NAME) sh -c "${SHELL_COMMAND}"
3637

3738
# Just to make sure it works.
3839
server: image
39-
docker run -it --rm $(LOCAL_LINK) $(PROJECT_NAME)
40+
./bin/run_in_shell
4041

4142
gem: image
4243
rm -f $(PROJECT_NAME)*.gem
43-
docker run $(LOCAL_LINK) $(PROJECT_NAME) gem build $(PROJECT_NAME)
44+
./bin/run_in_shell gem build $(PROJECT_NAME)
4445

4546
# Requires rubygems be installed on host
4647
gem_release: gem
47-
docker run -it --rm $(LOCAL_LINK) $(PROJECT_NAME) gem push $(PROJECT_NAME)*.gem
48+
./bin/run_in_shell gem push $(PROJECT_NAME)*.gem
4849

4950
publish_cross_platform_image:
50-
(docker buildx ls | grep mybuilder) || docker buildx create --name mybuilder
51-
docker buildx use mybuilder
52-
docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t kwerle/$(PROJECT_NAME) .
51+
(docker buildx ls | grep mybuilder) || ./bin/run_in_shell docker buildx create --name mybuilder
52+
./bin/run_in_shell docker buildx use mybuilder
53+
./bin/run_in_shell docker buildx build --push --platform linux/amd64,linux/arm64/v8 -t kwerle/$(PROJECT_NAME) .

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ Help welcome.
1515

1616
* Definitions
1717
* Completions
18-
* Lint - thanks to [RuboCop](https://github.com/bbatsov/rubocop)
1918
* Please see the [FAQ_ROADMAP.md](./FAQ_ROADMAP.md)
2019

2120
# Editor Integrations

bin/run_in_shell

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/sh
2+
# Mostly so AI can run commands in a consistent environment
3+
SHELL_COMMAND="$*"
4+
export SHELL_COMMAND
5+
make run_in_shell

lib/config/initializers/active_record.rb

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,19 @@
11
# frozen_string_literal: true
22

3+
extension_path = if Gem.win_platform?
4+
'liblevenshtein.dll'
5+
else
6+
'/usr/local/lib/liblevenshtein.so.0.0.0'
7+
end
8+
39
ActiveRecord::Base.establish_connection(
410
adapter: 'sqlite3',
511
database: '/database',
612
pool: 5,
7-
timeout: 30.seconds # does not seem to help
13+
timeout: 30.seconds, # does not seem to help
14+
extensions: [extension_path]
815
)
916

10-
ActiveSupport.on_load(:active_record) do
11-
database = ActiveRecord::Base.connection.raw_connection
12-
database.enable_load_extension(1)
13-
if Gem.win_platform?
14-
# load DLL from PATH
15-
database.load_extension('liblevenshtein.dll')
16-
else
17-
database.load_extension('/usr/local/lib/liblevenshtein.so.0.0.0')
18-
end
19-
database.enable_load_extension(0)
20-
end
21-
2217
if ENV['LOG_LEVEL'] == 'DEBUG'
2318
begin
2419
warn('Turning on active record logging to active_record.log')

lib/ruby_language_server/io.rb

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
# frozen_string_literal: true
22

33
require 'json'
4+
require 'socket'
45

56
module RubyLanguageServer
67
class IO
8+
attr_reader :using_socket
9+
710
def initialize(server, mutex)
811
@server = server
912
@mutex = mutex
1013
server.io = self
14+
15+
configure_io
16+
1117
loop do
12-
(id, response) = process_request($stdin)
13-
return_response(id, response, $stdout) unless id.nil?
18+
(id, response) = process_request
19+
return_response(id, response) unless id.nil?
1420
rescue SignalException => e
1521
RubyLanguageServer.logger.error "We received a signal. Let's bail: #{e}"
1622
exit
@@ -19,38 +25,66 @@ def initialize(server, mutex)
1925
backtrace = e.backtrace * "\n"
2026
RubyLanguageServer.logger.error "Backtrace:\n#{backtrace}"
2127
end
28+
return unless @using_socket
29+
30+
begin
31+
@in&.close
32+
rescue StandardError => e
33+
RubyLanguageServer.logger.error "Error closing socket: #{e}"
34+
end
2235
end
2336

24-
def return_response(id, response, io = $stdout)
37+
def send_notification(message, params)
38+
io ||= out
2539
full_response = {
2640
jsonrpc: '2.0',
27-
id:,
28-
result: response
41+
method: message,
42+
params:
2943
}
30-
response_body = JSON.unparse(full_response)
31-
RubyLanguageServer.logger.info "return_response body: #{response_body}"
32-
io.write "Content-Length: #{response_body.length + 0}\r\n"
44+
body = JSON.generate(full_response)
45+
RubyLanguageServer.logger.info "send_notification body: #{body}"
46+
io.write "Content-Length: #{body.length}\r\n"
3347
io.write "\r\n"
34-
io.write response_body
35-
io.flush
48+
io.write body
49+
io.flush if io.respond_to?(:flush)
50+
end
51+
52+
private
53+
54+
attr_accessor :in, :out
55+
56+
def configure_io
57+
if ENV['LSP_PORT']
58+
@tcp_server = TCPServer.new(ENV['LSP_PORT'].to_i)
59+
RubyLanguageServer.logger.info "Listening on TCP port #{ENV['LSP_PORT']} for LSP connections"
60+
self.in = @tcp_server.accept
61+
self.out = self.in
62+
@using_socket = true
63+
RubyLanguageServer.logger.info 'Accepted LSP socket connection'
64+
else
65+
self.in = $stdin
66+
self.out = $stdout
67+
@using_socket = false
68+
end
3669
end
3770

38-
def send_notification(message, params, io = $stdout)
71+
def return_response(id, response)
72+
io ||= out
3973
full_response = {
4074
jsonrpc: '2.0',
41-
method: message,
42-
params:
75+
id:,
76+
result: response
4377
}
44-
body = JSON.unparse(full_response)
45-
RubyLanguageServer.logger.info "send_notification body: #{body}"
46-
io.write "Content-Length: #{body.length + 0}\r\n"
78+
response_body = JSON.generate(full_response)
79+
RubyLanguageServer.logger.info "return_response body: #{response_body}"
80+
io.write "Content-Length: #{response_body.length}\r\n"
4781
io.write "\r\n"
48-
io.write body
49-
io.flush
82+
io.write response_body
83+
io.flush if io.respond_to?(:flush)
5084
end
5185

52-
def process_request(io = $stdin)
53-
request_body = get_request(io)
86+
def process_request
87+
request_body = get_request
5488
# RubyLanguageServer.logger.debug "request_body: #{request_body}"
5589
request_json = JSON.parse request_body
5690
id = request_json['id']
@@ -77,14 +111,14 @@ def process_request(io = $stdin)
77111
end
78112
end
79113

80-
def get_request(io = $stdin)
81-
initial_line = get_initial_request_line(io)
114+
def get_request # rubocop:disable Naming/AccessorMethodName
115+
initial_line = get_initial_request_line
82116
RubyLanguageServer.logger.debug "initial_line: #{initial_line}"
83117
length = get_length(initial_line)
84118
content = ''
85119
while content.length < length + 2
86120
begin
87-
content += get_content(length + 2, io) # Why + 2? CRLF?
121+
content += get_content(length + 2) # Why + 2? CRLF?
88122
rescue Exception => e
89123
RubyLanguageServer.logger.error e
90124
# We have almost certainly been disconnected from the server
@@ -95,8 +129,8 @@ def get_request(io = $stdin)
95129
content
96130
end
97131

98-
def get_initial_request_line(io = $stdin)
99-
io.gets
132+
def get_initial_request_line # rubocop:disable Naming/AccessorMethodName
133+
self.in.gets
100134
end
101135

102136
def get_length(string)
@@ -105,37 +139,8 @@ def get_length(string)
105139
string.match(/Content-Length: (\d+)/)[1].to_i
106140
end
107141

108-
def get_content(size, io = $stdin)
109-
io.read(size)
142+
def get_content(size)
143+
self.in.read(size)
110144
end
111-
112-
# http://www.alecjacobson.com/weblog/?p=75
113-
# def stdin_read_char
114-
# begin
115-
# # save previous state of stty
116-
# old_state = `stty -g`
117-
# # disable echoing and enable raw (not having to press enter)
118-
# system "stty raw -echo"
119-
# c = STDIN.getc.chr
120-
# # gather next two characters of special keys
121-
# if(c=="\e")
122-
# extra_thread = Thread.new{
123-
# c = c + STDIN.getc.chr
124-
# c = c + STDIN.getc.chr
125-
# }
126-
# # wait just long enough for special keys to get swallowed
127-
# extra_thread.join(0.00001)
128-
# # kill thread so not-so-long special keys don't wait on getc
129-
# extra_thread.kill
130-
# end
131-
# rescue Exception => ex
132-
# puts "#{ex.class}: #{ex.message}"
133-
# puts ex.backtrace
134-
# ensure
135-
# # restore previous state of stty
136-
# system "stty #{old_state}"
137-
# end
138-
# return c
139-
# end
140145
end # class
141146
end # module

0 commit comments

Comments
 (0)