lsp_router 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []