lsp_router 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d57746f846403f9ca2c5a45bba0ecd01fdb6e907e05095431063b8bc4a0a5f80
4
+ data.tar.gz: a3aa45e975f6db6712acd14edd1f5fcfa3603578572890c87c29d0e8abb34da9
5
+ SHA512:
6
+ metadata.gz: 101ee8a0b48df239a88ee5a4a684f3591ab544a7e76e35cbea4ab30e12ef4f0711fbde52dca246e0f5729d49589ada0d8327cf242ede5e6ef3f3ce7a5c2e0dea
7
+ data.tar.gz: dd2ec59d88c78cd1dd5111a0a6437ce87136b3ebc6fbdd1723b6a92e6d4a2e9ae5a6eff1c2c01ac61337e3e42dee342b8a4566b81e13367b03eb6154bcba149e
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,189 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ Bundler/OrderedGems:
5
+ Enabled: false
6
+
7
+ Gemspec/DevelopmentDependencies:
8
+ EnforcedStyle: gems.rb
9
+
10
+ Layout/EmptyLineAfterGuardClause:
11
+ Enabled: false
12
+
13
+ Layout/ExtraSpacing:
14
+ AllowBeforeTrailingComments: true
15
+
16
+ Layout/LineLength:
17
+ Max: 130
18
+
19
+ Layout/MultilineMethodCallIndentation:
20
+ EnforcedStyle: indented_relative_to_receiver
21
+
22
+ Layout/SpaceAroundEqualsInParameterDefault:
23
+ Enabled: false
24
+
25
+ Layout/SpaceAroundOperators:
26
+ Enabled: false
27
+
28
+ Layout/SpaceBeforeBlockBraces:
29
+ Enabled: false
30
+
31
+ Layout/SpaceInsideBlockBraces:
32
+ Enabled: false
33
+
34
+ Layout/HashAlignment:
35
+ Enabled: false
36
+ EnforcedHashRocketStyle: table
37
+ EnforcedColonStyle: table
38
+
39
+ Layout/SpaceAfterNot:
40
+ Enabled: false
41
+
42
+ Layout/SpaceInsideHashLiteralBraces:
43
+ Enabled: false
44
+
45
+ Layout/SpaceInsidePercentLiteralDelimiters:
46
+ Enabled: false
47
+
48
+ Lint/SuppressedException:
49
+ AllowComments: true
50
+
51
+ Metrics/AbcSize:
52
+ Max: 60
53
+
54
+ Metrics/BlockNesting:
55
+ Enabled: false
56
+
57
+ Metrics/ClassLength:
58
+ Enabled: false
59
+
60
+ Metrics/CyclomaticComplexity:
61
+ Max: 20
62
+
63
+ Metrics/MethodLength:
64
+ Max: 100
65
+
66
+ Naming/AccessorMethodName:
67
+ Enabled: false
68
+
69
+ Naming/BlockForwarding:
70
+ Enabled: false
71
+
72
+ Naming/MethodParameterName:
73
+ Enabled: false
74
+
75
+ Metrics/ParameterLists:
76
+ Max: 20
77
+
78
+ Metrics/PerceivedComplexity:
79
+ Max: 100
80
+
81
+ Style/AndOr:
82
+ Enabled: false
83
+
84
+ Style/BlockDelimiters:
85
+ Enabled: false
86
+
87
+ Style/CaseLikeIf:
88
+ Enabled: false
89
+
90
+ Style/ClassAndModuleChildren:
91
+ Enabled: false
92
+
93
+ Style/ClassCheck:
94
+ Enabled: false
95
+
96
+ Style/ClassMethods:
97
+ Enabled: false
98
+
99
+ Style/CommentedKeyword:
100
+ Enabled: false
101
+
102
+ Style/FormatString:
103
+ Enabled: false
104
+
105
+ Style/FrozenStringLiteralComment:
106
+ Enabled: false
107
+
108
+ Style/GuardClause:
109
+ Enabled: false
110
+
111
+ Style/IfUnlessModifier:
112
+ Enabled: false
113
+
114
+ Style/InfiniteLoop:
115
+ Enabled: false
116
+
117
+ Style/Lambda:
118
+ Enabled: false
119
+
120
+ Style/MethodCallWithoutArgsParentheses:
121
+ Enabled: false
122
+
123
+ Style/MutableConstant:
124
+ Enabled: false
125
+
126
+ Style/NestedTernaryOperator:
127
+ Enabled: false
128
+
129
+ Style/Next:
130
+ Enabled: false
131
+
132
+ Style/Not:
133
+ Enabled: false
134
+
135
+ Style/NumericLiterals:
136
+ Enabled: false
137
+
138
+ Style/NumericPredicate:
139
+ Enabled: false
140
+
141
+ Style/ParallelAssignment:
142
+ Enabled: false
143
+
144
+ Style/PercentLiteralDelimiters:
145
+ Enabled: false
146
+
147
+ Style/PerlBackrefs:
148
+ Enabled: false
149
+
150
+ Style/PreferredHashMethods:
151
+ Enabled: false
152
+
153
+ Style/RedundantReturn:
154
+ Enabled: false
155
+
156
+ Style/RedundantSelf:
157
+ Enabled: false
158
+
159
+ Style/RescueModifier:
160
+ Enabled: false
161
+
162
+ Style/RescueStandardError:
163
+ Enabled: false
164
+
165
+ Style/SafeNavigation:
166
+ Enabled: false
167
+
168
+ Style/SpecialGlobalVars:
169
+ Enabled: false
170
+
171
+ Style/StringConcatenation:
172
+ Enabled: false
173
+
174
+ Style/StringLiterals:
175
+ Enabled: false
176
+
177
+ Style/TrailingCommaInArrayLiteral:
178
+ EnforcedStyleForMultiline: consistent_comma
179
+
180
+ Style/TrailingCommaInHashLiteral:
181
+ EnforcedStyleForMultiline: consistent_comma
182
+
183
+ Style/VariableInterpolation:
184
+ Enabled: false
185
+
186
+ Style/WordArray:
187
+ Enabled: false
188
+
189
+
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # LspRouter
2
+
3
+ 複数のLSPサーバーに処理を振り分けるルーター
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ gem install lsp_router
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ 次のような config ファイルを作成して、`lsp_router config-file` のように実行する。
14
+
15
+ ```ruby
16
+ logfile '/tmp/lsp_router'
17
+ loglevel :info
18
+
19
+ server :rubocop do
20
+ command 'rubocop-lsp'
21
+ mode :stdio
22
+ end
23
+
24
+ server :solargraph do
25
+ command 'solargraph stdio'
26
+ mode :stdio
27
+ end
28
+ ```
29
+
30
+ この例では、`rubocop-lsp` と `solargraph stdio` を起動して、クライアントからの処理を振り分ける。
31
+ 最初に各サーバーの capabilities を確認して、各サーバーにどの機能があるかを確認し、クライアントからの REQUEST は対応しているサーバーに渡す。複数のサーバーが同じ機能を持っていれば上に書いたサーバーが優先される。NOTIFICATION は全サーバーに渡す。
32
+
33
+ ## Example
34
+
35
+ Emacs の Eglot の場合はこんな感じに設定するといいっぽい。
36
+
37
+ ```
38
+ (add-to-list 'eglot-server-programs '((ruby-mode ruby-ts-mode) . ("lsp_router" "--error=lsp_router.err" "lsp_router.conf")))
39
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,12 @@
1
+ logfile '/tmp/lsp_router'
2
+ loglevel :info
3
+
4
+ server :rubocop do
5
+ command 'rubocop-lsp'
6
+ mode :stdio
7
+ end
8
+
9
+ server :solargraph do
10
+ command 'solargraph stdio'
11
+ mode :stdio
12
+ end
data/exe/lsp_router ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ require 'lsp_router'
3
+ require 'lsp_router/cli'
4
+
5
+ LspRouter::CLI.start
@@ -0,0 +1,31 @@
1
+ require 'optparse'
2
+
3
+ class LspRouter
4
+ # command line interface
5
+ class CLI
6
+ def self.start
7
+ port = nil
8
+ logfile = nil
9
+ error = nil
10
+
11
+ opts = OptionParser.new
12
+ opts.version = LspRouter::VERSION
13
+ opts.banner = "Usage: #{File.basename $0} [options] config_file"
14
+ opts.on('-p', '--port=port'){|v| port = v}
15
+ opts.on('--logfile=file'){|v| logfile = v}
16
+ opts.on('--error=file'){|v| error = v}
17
+ opts.parse!(ARGV)
18
+ if error
19
+ $stderr = File.open(error, 'a')
20
+ $stderr.sync = true
21
+ end
22
+ if ARGV.size != 1
23
+ puts opts.help
24
+ exit 1
25
+ end
26
+ config_file, = ARGV
27
+ load config_file, LspRouter::Config::M
28
+ LspRouter.new(LspRouter::Config.instance, port:, logfile:).run
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,50 @@
1
+ class LspRouter
2
+ # client side
3
+ class ClientSide
4
+ # @param rio [IO]
5
+ # @param wio [IO]
6
+ # @param logger [Logger]
7
+ def initialize(rio, wio, logger:)
8
+ @logger = logger
9
+ @rio = rio
10
+ @wio = wio
11
+ end
12
+
13
+ # @return [Hash]
14
+ def read
15
+ header = @rio.gets("\r\n\r\n")
16
+ return unless header
17
+ fields = header.lines.map do |line|
18
+ n, v = line.chomp.split(/: */, 2)
19
+ [n.downcase, v] if n
20
+ end.compact.to_h
21
+ ret = @rio.read(fields['content-length'].to_i)
22
+ return unless ret
23
+ data = JSON.parse(ret)
24
+ log('client>', data)
25
+ data
26
+ end
27
+
28
+ # @param data [Hash]
29
+ def write(data)
30
+ log('client<', data)
31
+ json = data.to_json
32
+ @wio.puts "Content-Length: #{json.bytesize}\r\n\r\n"
33
+ @wio.write(json)
34
+ @wio.flush
35
+ end
36
+
37
+ # @param prefix [String]
38
+ # @param data [Hash]
39
+ def log(prefix, data)
40
+ if data['id'] && data['method']
41
+ @logger.debug "#{prefix}[REQUEST] id:#{data['id']} method:#{data['method']}"
42
+ elsif data['id']
43
+ @logger.debug "#{prefix}[RESPONSE] id:#{data['id']}"
44
+ else
45
+ @logger.debug "#{prefix}[NOTIFICATION] method:#{data['method']}"
46
+ end
47
+ @logger.debug data.inspect
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,69 @@
1
+ require 'singleton'
2
+
3
+ class LspRouter
4
+ # config
5
+ class Config
6
+ include Singleton
7
+
8
+ attr_reader :servers
9
+ attr_accessor :logfile, :loglevel
10
+
11
+ def initialize
12
+ @servers = []
13
+ end
14
+
15
+ # @param name [String]
16
+ def add_server(name, &block)
17
+ @servers.push Server.new(name, &block)
18
+ end
19
+
20
+ # module for configuration file
21
+ module M
22
+ # @param name [String]
23
+ def server(name, &block)
24
+ config.add_server(name, &block)
25
+ end
26
+
27
+ # @param filename [String]
28
+ def logfile(filename)
29
+ config.logfile = filename
30
+ end
31
+
32
+ # @param level [Symbol]
33
+ def loglevel(level)
34
+ config.loglevel = level
35
+ end
36
+
37
+ # @return [LspRouter::Config]
38
+ def config
39
+ Config.instance
40
+ end
41
+ end
42
+
43
+ # server
44
+ class Server
45
+ attr_reader :attr
46
+
47
+ # @param name [String]
48
+ def initialize(name, &block)
49
+ @attr = {name:}
50
+ self.instance_eval(&block)
51
+ end
52
+
53
+ # @param cmd [String]
54
+ def command(cmd)
55
+ @attr[:command] = cmd
56
+ end
57
+
58
+ # @param m [Symbol]
59
+ def mode(m)
60
+ @attr[:mode] = m
61
+ end
62
+
63
+ # @return [String]
64
+ def info
65
+ "[#{attr[:name]}] #{attr[:command]}"
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,66 @@
1
+ require 'open3'
2
+
3
+ class LspRouter
4
+ # server side
5
+ class ServerSide
6
+ # @return String
7
+ attr_reader :name
8
+ # @return [Hash]
9
+ attr_reader :capabilities
10
+
11
+ # @param server [LspRouter::Config::Server]
12
+ # @param logger [Logger]
13
+ def initialize(server, logger:)
14
+ @logger = logger
15
+ @name = server.attr[:name]
16
+ @command = server.attr[:command]
17
+ @stdin, @stdout, @stderr = Open3.popen3(@command)
18
+ @capabilities = {}
19
+ end
20
+
21
+ # @param req [Hash]
22
+ def init(req)
23
+ write(req)
24
+ data = read
25
+ capa = data.dig('result', 'capabilities')
26
+ capa.keys.grep(/Provider/).each do |key|
27
+ @capabilities[key.sub(/Provider\z/, '')] = true
28
+ end
29
+ data
30
+ end
31
+
32
+ # @param data [Hash]
33
+ def write(data)
34
+ log("#{name}<", data)
35
+ json = data.to_json
36
+ @stdin.puts "Content-Length: #{json.bytesize}\r\n\r\n"
37
+ @stdin.write(json)
38
+ @stdin.flush
39
+ end
40
+
41
+ # @return [Hash]
42
+ def read
43
+ header = @stdout.gets("\r\n\r\n")
44
+ fields = header.lines.map do |line|
45
+ n, v = line.chomp.split(/: */, 2)
46
+ [n.downcase, v] if n
47
+ end.compact.to_h
48
+ data = JSON.parse(@stdout.read(fields['content-length'].to_i))
49
+ log("#{name}>", data)
50
+ data
51
+ end
52
+
53
+ # @param prefix [String]
54
+ # @param data [Hash]
55
+ def log(prefix, data)
56
+ if data['id'] && data['method']
57
+ @logger.debug "#{prefix}[REQUEST] id:#{data['id']} method:#{data['method']}"
58
+ elsif data['id']
59
+ @logger.debug "#{prefix}[RESPONSE] id:#{data['id']}"
60
+ else
61
+ @logger.debug "#{prefix}[NOTIFICATION] method:#{data['method']}"
62
+ end
63
+ @logger.debug data.inspect
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,3 @@
1
+ class LspRouter
2
+ VERSION = '0.1.0'
3
+ end
data/lib/lsp_router.rb ADDED
@@ -0,0 +1,94 @@
1
+ require_relative 'lsp_router/version'
2
+ require_relative 'lsp_router/config'
3
+ require_relative 'lsp_router/server_side'
4
+ require_relative 'lsp_router/client_side'
5
+
6
+ require 'socket'
7
+ require 'json'
8
+ require 'logger'
9
+
10
+ # LspRouter
11
+ class LspRouter
12
+ # @param config [LspRouter::Config] configuration filename
13
+ # @param port [Integer, String]
14
+ # @param logfile [String]
15
+ def initialize(config, port: nil, logfile: nil)
16
+ @config = config
17
+ @port = port
18
+ logfile ||= config.logfile || $stderr
19
+ @logger = Logger.new(logfile, level: Logger.const_get(config.loglevel.upcase))
20
+ @logger.info "LspRouter: start"
21
+ @config.servers.each do |server|
22
+ @logger.info "server: #{server.info}"
23
+ end
24
+ end
25
+
26
+ def run
27
+ if @port
28
+ @logger.info "mode: port:#{@port}"
29
+ Socket.tcp_server_loop(@port) do |sock, _|
30
+ @client = LspRouter::ClientSide.new(sock, sock, logger: @logger)
31
+ loop
32
+ end
33
+ else
34
+ @logger.info "mode: stdio"
35
+ @client = LspRouter::ClientSide.new($stdin, $stdout, logger: @logger)
36
+ loop
37
+ end
38
+ rescue => e
39
+ @logger.error e
40
+ ensure
41
+ @logger.info "LspRouter: stop"
42
+ end
43
+
44
+ def loop
45
+ req = @client.read
46
+ servers = @config.servers.map{LspRouter::ServerSide.new(_1, logger: @logger)}
47
+ primary = servers.first
48
+
49
+ res = nil
50
+ capa_server = {}
51
+ servers.each do |server|
52
+ data = server.init(req)
53
+ if res
54
+ res['result']['capabilities'].update(data.dig('result', 'capabilities')){|_, v, _| v}
55
+ else
56
+ res = data
57
+ end
58
+ server.capabilities.each_key do |key|
59
+ capa_server[key] ||= server
60
+ end
61
+ end
62
+ @client.write(res)
63
+
64
+ thr = Thread.new do
65
+ Thread.abort_on_exception = true
66
+ while true
67
+ data = @client.read
68
+ break unless data
69
+ if data['id']
70
+ # request
71
+ method = data['method']&.split('/')&.last
72
+ server = capa_server[method] || primary
73
+ server.write(data)
74
+ else
75
+ # notification
76
+ servers.each do |server|
77
+ server.write(data)
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ servers.each do |server|
84
+ Thread.new do
85
+ Thread.abort_on_exception = true
86
+ while true
87
+ data = server.read
88
+ @client.write(data)
89
+ end
90
+ end
91
+ end
92
+ thr.join
93
+ end
94
+ end
@@ -0,0 +1,4 @@
1
+ module LspRouter
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lsp_router
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - TOMITA Masahiro
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-12-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: routing multiple LSP server by capability
14
+ email:
15
+ - tommy@tmtm.org
16
+ executables:
17
+ - lsp_router
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".rspec"
22
+ - ".rubocop.yml"
23
+ - README.md
24
+ - Rakefile
25
+ - config/example.conf
26
+ - exe/lsp_router
27
+ - lib/lsp_router.rb
28
+ - lib/lsp_router/cli.rb
29
+ - lib/lsp_router/client_side.rb
30
+ - lib/lsp_router/config.rb
31
+ - lib/lsp_router/server_side.rb
32
+ - lib/lsp_router/version.rb
33
+ - sig/lsp_router.rbs
34
+ homepage: https://gitlab.com/tmtms/lsp_router
35
+ licenses:
36
+ - GPLv3
37
+ metadata:
38
+ homepage_uri: https://gitlab.com/tmtms/lsp_router
39
+ source_code_uri: https://gitlab.com/tmtms/lsp_router
40
+ rubygems_mfa_required: 'true'
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '3.1'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubygems_version: 3.5.0.dev
57
+ signing_key:
58
+ specification_version: 4
59
+ summary: LSP router
60
+ test_files: []