refined-steep-server 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: d0c63912a979bde112ef66e36710f04338a7f2a48a7b8a5e3eedcab49ce5d1d1
4
+ data.tar.gz: c210c1c303407527894df79ec9080a3ba98832b8b3216fdf21d066ea9b1df75c
5
+ SHA512:
6
+ metadata.gz: 85a0d7823de325492f7b94e3c76b242d5fecf63cfc165b2ad746b69ab2bf0bb48add4a97debc1102cfaf12dc678520150d7d0343feea667f4beb8d49cca2ff80
7
+ data.tar.gz: a94ed92dcad7a6081ca197fe4152514a45163bd2df73ca4f7913a3773714c0744b89e644caba9691e6c36fa6370beb230aadd3b3baf6970da68a39646ae95061
@@ -0,0 +1,7 @@
1
+ Steepの内部APIを新たに利用するコードが追加された場合に、`sig/external/steep.rbs` に不足しているスタブRBSを追加して。
2
+
3
+ 手順:
4
+ 1. `bundle exec steep check` を実行してUnknownConstantやUnknownTypeNameエラーを確認
5
+ 2. エラーに対応するSteepの内部クラス・メソッドの実際のシグネチャを参照ソース(/home/joker/ghq/github.com/soutaro/steep)から調査
6
+ 3. `sig/external/steep.rbs` に必要最小限のスタブを追加
7
+ 4. 再度 `bundle exec steep check` を実行してエラーが解消されたことを確認
@@ -0,0 +1,7 @@
1
+ rbs-inline生成、steep型チェック、rspecテストを順に実行して結果を報告して。
2
+
3
+ 1. `bundle exec rbs-inline --output=sig/generated lib` でRBSを再生成
4
+ 2. `bundle exec steep check` で型チェック
5
+ 3. `bundle exec rspec` でテスト実行
6
+
7
+ いずれかが失敗した場合はその時点で止めて、エラー内容を報告して。
@@ -0,0 +1,55 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+ checks: write
12
+
13
+ jobs:
14
+ test:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ matrix:
18
+ ruby: ["3.2", "3.3", "3.4", "4.0"]
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: ruby/setup-ruby@v1
22
+ with:
23
+ ruby-version: ${{ matrix.ruby }}
24
+ bundler-cache: true
25
+ - name: Run rspec
26
+ run: bundle exec rspec --format documentation --format RspecJunitFormatter --out tmp/rspec-results.xml
27
+ - name: Upload test results
28
+ uses: dorny/test-reporter@v1
29
+ if: always()
30
+ with:
31
+ name: "RSpec Results (Ruby ${{ matrix.ruby }})"
32
+ path: tmp/rspec-results.xml
33
+ reporter: java-junit
34
+
35
+ typecheck:
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+ - uses: ruby/setup-ruby@v1
40
+ with:
41
+ ruby-version: "3.4"
42
+ bundler-cache: true
43
+ - name: Generate RBS with rbs-inline
44
+ run: bundle exec rbs-inline --output=sig/generated lib
45
+ - name: Cache RBS collection
46
+ uses: actions/cache@v4
47
+ with:
48
+ path: .gem_rbs_collection
49
+ key: rbs-collection-${{ hashFiles('rbs_collection.lock.yaml') }}
50
+ restore-keys: |
51
+ rbs-collection-
52
+ - name: Install RBS collection
53
+ run: bundle exec rbs collection install
54
+ - name: Run steep check
55
+ run: bundle exec steep check
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/CLAUDE.md ADDED
@@ -0,0 +1,57 @@
1
+ # refined-steep-server
2
+
3
+ Steepをライブラリとして利用して、Steep組込みと同等の機能を持ったlanguage server。
4
+ 特にneovimとの親和性を意識している。
5
+ 但しlanguage serverの実装はruby-lspを参考にする。
6
+
7
+ ## 参照するソースコード
8
+
9
+ - Steep: /home/joker/ghq/github.com/soutaro/steep
10
+ - ruby-lsp: /home/joker/ghq/github.com/Shopify/ruby-lsp
11
+ - rbs-inline: /home/joker/ghq/github.com/soutaro/rbs-inline
12
+
13
+ ## アーキテクチャ
14
+
15
+ - ruby-lsp方式のシングルプロセス・マルチスレッドモデル
16
+ - メインスレッド: stdinからLSPメッセージ読み取り → incoming_queueへ
17
+ - ワーカースレッド: incoming_queueから取り出し → process_message → Steepサービス呼び出し
18
+ - ライタースレッド: outgoing_queueから取り出し → stdoutへJSON-RPC書き込み
19
+ - Steepのサービス群(TypeCheckService, HoverProvider, CompletionProvider, GotoService, SignatureHelpProvider)をライブラリとして直接利用
20
+ - PathAssignment.allで全ファイルを単一プロセスで処理
21
+
22
+ ## 対応LSP機能
23
+
24
+ - textDocument/hover
25
+ - textDocument/completion (トリガー: `.`, `@`, `:`)
26
+ - textDocument/signatureHelp (トリガー: `(`)
27
+ - textDocument/definition
28
+ - textDocument/implementation
29
+ - textDocument/typeDefinition
30
+ - workspace/symbol
31
+ - textDocument/publishDiagnostics
32
+
33
+ ## 型注釈
34
+
35
+ - rbs-inline方式で型注釈を記述
36
+ - 各Rubyファイル先頭に `# rbs_inline: enabled` マーカーを記述
37
+ - `sig/generated/` にrbs-inlineで生成したRBSを出力(.gitignore対象)
38
+ - `sig/external/steep.rbs` にsteep・language_server-protocol・parser gemの手書きスタブRBSを配置
39
+ - steep gemはsig/をgemパッケージに含めていないため、RBS collectionでは型定義を取得できない
40
+ - Steepの内部APIを新たに利用する場合はこのファイルにスタブを追加すること
41
+
42
+ ## テスト・型チェック
43
+
44
+ - rspecを使用
45
+ - 実装時は各ステップでテスト通過を確認しながら進める
46
+ - steep checkはエラーなしの状態を維持する
47
+
48
+ ## コマンド
49
+
50
+ - `bundle exec rspec` — テスト実行
51
+ - `bundle exec rbs-inline --output=sig/generated lib` — rbs-inlineでRBS生成
52
+ - `bundle exec steep check` — 型チェック
53
+ - 実装変更後のワークフロー: `rbs-inline生成 → steep check → rspec` の順に実行
54
+
55
+ ## Commit Comment
56
+
57
+ Conventional Commitsの規約に従う。
data/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # refined-steep-server
2
+
3
+ [![CI](https://github.com/joker1007/refined-steep-server/actions/workflows/ci.yml/badge.svg)](https://github.com/joker1007/refined-steep-server/actions/workflows/ci.yml)
4
+
5
+ Most of this project is written by Claude Code.
6
+
7
+ A language server for Ruby type checking that uses [Steep](https://github.com/soutaro/steep) as a library. It provides the same type checking features as Steep's built-in language server, reimplemented with a single-process multi-threaded architecture inspired by [ruby-lsp](https://github.com/Shopify/ruby-lsp). Designed with Neovim compatibility in mind.
8
+
9
+ ## Motivation
10
+
11
+ Steep ships with a built-in language server based on a multi-process architecture (master + interaction worker + type-check workers). While powerful, this design can be complex to manage and debug. It also type-checks the entire project on startup, which can cause significant delays in large codebases.
12
+
13
+ refined-steep-server takes a different approach: it runs everything in a single process with multiple threads, directly calling Steep's internal services as a library. Instead of type-checking all files on startup, it only type-checks files as they are opened or modified. This results in a simpler, faster-starting server that is easier to integrate with editors like Neovim.
14
+
15
+ ## Features
16
+
17
+ ### Supported LSP Methods
18
+
19
+ | Method | Description |
20
+ |--------|-------------|
21
+ | `textDocument/hover` | Show type information at cursor |
22
+ | `textDocument/completion` | Code completion (triggers: `.`, `@`, `:`) |
23
+ | `textDocument/signatureHelp` | Method signature help (trigger: `(`) |
24
+ | `textDocument/definition` | Go to definition |
25
+ | `textDocument/implementation` | Find implementations |
26
+ | `textDocument/typeDefinition` | Go to type definition |
27
+ | `workspace/symbol` | Workspace-wide symbol search |
28
+ | `textDocument/publishDiagnostics` | Type error diagnostics |
29
+
30
+ ### Additional Features
31
+
32
+ - Incremental text document synchronization
33
+ - Type checking runs on file open and file save (not on every keystroke)
34
+ - WorkDoneProgress notifications for type checking progress
35
+ - Steepfile auto-discovery (searches parent directories)
36
+ - Configurable logging with `--log-level` and `--log-file` options
37
+
38
+ ## Architecture
39
+
40
+ ```
41
+ Client (editor)
42
+ |
43
+ v
44
+ [Main Thread] reads stdin --> incoming_queue
45
+ |
46
+ v
47
+ [Worker Thread] pops from incoming_queue --> process_message --> Steep services
48
+ |
49
+ v
50
+ [Writer Thread] pops from outgoing_queue --> writes JSON-RPC to stdout
51
+ ```
52
+
53
+ Steep's services (`TypeCheckService`, `HoverProvider`, `CompletionProvider`, `GotoService`, `SignatureHelpProvider`) are called directly as library APIs, with `PathAssignment.all` handling all files in a single process.
54
+
55
+ ## Requirements
56
+
57
+ - Ruby >= 3.2.0
58
+ - Steep ~> 1.10
59
+ - A `Steepfile` in your project
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ gem install refined-steep-server
65
+ ```
66
+
67
+ Or add to your Gemfile:
68
+
69
+ ```ruby
70
+ gem "refined-steep-server"
71
+ ```
72
+
73
+ ## Usage
74
+
75
+ ### Basic
76
+
77
+ ```bash
78
+ refined-steep-server
79
+ ```
80
+
81
+ The server communicates over stdin/stdout using the LSP protocol. Point your editor's LSP client to this executable.
82
+
83
+ ### With Debug Logging
84
+
85
+ ```bash
86
+ refined-steep-server --log-level debug
87
+ ```
88
+
89
+ ### With Log File
90
+
91
+ ```bash
92
+ refined-steep-server --log-level debug --log-file /tmp/refined-steep.log
93
+ ```
94
+
95
+ ### Neovim Configuration (0.12+)
96
+
97
+ Neovim 0.12+ has built-in LSP support via `vim.lsp.config()` and `vim.lsp.enable()`. No plugins required.
98
+
99
+ ```lua
100
+ vim.lsp.config("refined_steep", {
101
+ cmd = { "refined-steep-server" },
102
+ filetypes = { "ruby" },
103
+ root_markers = { "Steepfile" },
104
+ })
105
+
106
+ vim.lsp.enable("refined_steep")
107
+ ```
108
+
109
+ ## Development
110
+
111
+ After checking out the repo, run `bin/setup` to install dependencies.
112
+
113
+ ### Commands
114
+
115
+ ```bash
116
+ # Run tests
117
+ bundle exec rspec
118
+
119
+ # Generate RBS from inline annotations
120
+ bundle exec rbs-inline --output=sig/generated lib
121
+
122
+ # Run type checker
123
+ bundle exec steep check
124
+
125
+ # Recommended workflow after changes:
126
+ bundle exec rbs-inline --output=sig/generated lib && bundle exec steep check && bundle exec rspec
127
+ ```
128
+
129
+ ### Project Structure
130
+
131
+ ```
132
+ lib/refined/steep/server/
133
+ base_server.rb # Abstract server with 3-thread model (reader/worker/writer)
134
+ lsp_server.rb # Concrete LSP server with request routing and handlers
135
+ steep_state.rb # Bridge to Steep: Project, TypeCheckService, change buffer
136
+ store.rb # Document state management
137
+ message.rb # LSP message types (Result, Error, Notification, Request)
138
+ io.rb # JSON-RPC MessageReader/MessageWriter
139
+ ```
140
+
141
+ ## Contributing
142
+
143
+ Bug reports and pull requests are welcome on GitHub at https://github.com/joker1007/refined-steep-server.
144
+
145
+ ## License
146
+
147
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ namespace :rbs do
6
+ desc "Generate RBS files from rbs-inline annotations"
7
+ task :inline do
8
+ sh "bundle exec rbs-inline --output=sig/generated lib"
9
+ end
10
+ end
11
+
12
+ task default: %i[]
data/Steepfile ADDED
@@ -0,0 +1,5 @@
1
+ target :lib do
2
+ check "lib"
3
+ signature "sig"
4
+ collection_config "rbs_collection.yaml"
5
+ end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ require "logger"
6
+ require "refined/steep/server"
7
+ require "refined/steep/server/lsp_server"
8
+
9
+ log_level = Logger::WARN
10
+ log_output = $stderr
11
+
12
+ OptionParser.new do |opts|
13
+ opts.banner = "Usage: refined-steep-server [options]"
14
+
15
+ opts.on("--log-level LEVEL", %w[debug info warn error fatal], "Set log level (debug, info, warn, error, fatal)") do |level|
16
+ log_level = Logger.const_get(level.upcase)
17
+ end
18
+
19
+ opts.on("--log-file PATH", "Log to file instead of stderr") do |path|
20
+ log_output = File.open(path, "a")
21
+ log_output.sync = true
22
+ end
23
+ end.parse!
24
+
25
+ $stdin.sync = true
26
+ $stdout.sync = true
27
+ $stderr.sync = true
28
+ $stdin.binmode
29
+ $stdout.binmode
30
+ $stderr.binmode
31
+
32
+ logger = Refined::Steep::Server::BaseServer.create_default_logger(level: log_level, io: log_output)
33
+ logger.info { "Starting refined-steep-server v#{Refined::Steep::Server::VERSION} (log_level=#{Logger::SEV_LABEL[log_level]})" }
34
+
35
+ server = Refined::Steep::Server::LspServer.new(logger: logger)
36
+ server.start
@@ -0,0 +1,171 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "logger"
5
+
6
+ module Refined
7
+ module Steep
8
+ module Server
9
+ # @rbs!
10
+ # type lsp_message = Hash[Symbol, untyped]
11
+
12
+ class BaseServer
13
+ # @rbs @reader: MessageReader
14
+ # @rbs @writer: MessageWriter
15
+ # @rbs @mutex: Mutex
16
+ # @rbs @incoming_queue: Thread::Queue
17
+ # @rbs @outgoing_queue: Thread::Queue
18
+ # @rbs @cancelled_requests: Array[Integer]
19
+ # @rbs @current_request_id: Integer
20
+ # @rbs @worker: Thread
21
+ # @rbs @outgoing_dispatcher: Thread
22
+
23
+ attr_reader :logger #: Logger
24
+
25
+ # @rbs reader: IO?
26
+ # @rbs writer: IO?
27
+ # @rbs logger: Logger?
28
+ # @rbs return: void
29
+ def initialize(reader: nil, writer: nil, logger: nil)
30
+ @logger = logger || self.class.create_default_logger
31
+ @reader = MessageReader.new(reader || $stdin)
32
+ @writer = MessageWriter.new(writer || $stdout)
33
+ @mutex = Mutex.new
34
+ @incoming_queue = Thread::Queue.new
35
+ @outgoing_queue = Thread::Queue.new
36
+ @cancelled_requests = []
37
+ @current_request_id = 1
38
+
39
+ @worker = start_worker_thread
40
+ @outgoing_dispatcher = start_outgoing_thread
41
+
42
+ Thread.main.priority = 1
43
+ end
44
+
45
+ # @rbs level: Integer
46
+ # @rbs io: IO
47
+ # @rbs return: Logger
48
+ def self.create_default_logger(level: Logger::WARN, io: $stderr)
49
+ l = Logger.new(io)
50
+ l.level = level
51
+ l.formatter = proc do |severity, datetime, _progname, msg|
52
+ "[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] #{severity} -- #{msg}\n"
53
+ end
54
+ l
55
+ end
56
+
57
+ # @rbs return: void
58
+ def start
59
+ @reader.each_message do |message|
60
+ method = message[:method]
61
+ logger.debug { "Received: method=#{method} id=#{message[:id]}" }
62
+
63
+ case method
64
+ when "initialize", "initialized", "$/cancelRequest"
65
+ process_message(message)
66
+ when "shutdown"
67
+ @mutex.synchronize do
68
+ logger.info { "Shutting down refined-steep-server..." }
69
+ send_log_message("Shutting down refined-steep-server...")
70
+ shutdown
71
+ run_shutdown
72
+ @writer.write(Result.new(id: message[:id], response: nil).to_hash)
73
+ end
74
+ when "exit"
75
+ logger.info { "Exiting" }
76
+ exit(@incoming_queue.closed? ? 0 : 1)
77
+ else
78
+ @incoming_queue << message
79
+ end
80
+ end
81
+ end
82
+
83
+ # @rbs message: lsp_message
84
+ # @rbs return: void
85
+ def process_message(message)
86
+ raise NotImplementedError
87
+ end
88
+
89
+ # @rbs return: void
90
+ def run_shutdown
91
+ @incoming_queue.clear
92
+ @outgoing_queue.clear
93
+ @incoming_queue.close
94
+ @outgoing_queue.close
95
+ @cancelled_requests.clear
96
+
97
+ @worker.terminate
98
+ @outgoing_dispatcher.terminate
99
+ end
100
+
101
+ private
102
+
103
+ # @rbs return: void
104
+ def shutdown
105
+ raise NotImplementedError
106
+ end
107
+
108
+ # @rbs return: Thread
109
+ def start_worker_thread
110
+ Thread.new do
111
+ while (message = @incoming_queue.pop)
112
+ handle_incoming_message(message)
113
+ end
114
+ end
115
+ end
116
+
117
+ # @rbs return: Thread
118
+ def start_outgoing_thread
119
+ Thread.new do
120
+ while (message = @outgoing_queue.pop)
121
+ @mutex.synchronize { @writer.write(message.to_hash) }
122
+ end
123
+ end
124
+ end
125
+
126
+ # @rbs message: lsp_message
127
+ # @rbs return: void
128
+ def handle_incoming_message(message)
129
+ id = message[:id]
130
+
131
+ @mutex.synchronize do
132
+ if id && @cancelled_requests.delete(id)
133
+ logger.debug { "Request #{id} was cancelled, skipping" }
134
+ send_message(ErrorResponse.new(
135
+ id: id,
136
+ code: Constant::ErrorCodes::REQUEST_CANCELLED,
137
+ message: "Request #{id} was cancelled",
138
+ ))
139
+ return
140
+ end
141
+ end
142
+
143
+ process_message(message)
144
+ @cancelled_requests.delete(id)
145
+ end
146
+
147
+ # @rbs message: Result | ErrorResponse | Notification | Request
148
+ # @rbs return: void
149
+ def send_message(message)
150
+ return if @outgoing_queue.closed?
151
+
152
+ @outgoing_queue << message
153
+ @current_request_id += 1 if message.is_a?(Request)
154
+ end
155
+
156
+ # @rbs id: Integer
157
+ # @rbs return: void
158
+ def send_empty_response(id)
159
+ send_message(Result.new(id: id, response: nil))
160
+ end
161
+
162
+ # @rbs message: String
163
+ # @rbs type: Integer
164
+ # @rbs return: void
165
+ def send_log_message(message, type: Constant::MessageType::LOG)
166
+ send_message(Notification.window_log_message(message, type: type))
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,54 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+
6
+ module Refined
7
+ module Steep
8
+ module Server
9
+ class MessageReader
10
+ # @rbs @io: IO
11
+
12
+ # @rbs io: IO
13
+ # @rbs return: void
14
+ def initialize(io)
15
+ @io = io
16
+ end
17
+
18
+ # @rbs &block: (Hash[Symbol, untyped]) -> void
19
+ # @rbs return: void
20
+ def each_message(&block)
21
+ while (headers = @io.gets("\r\n\r\n"))
22
+ content_length = headers[/Content-Length: (\d+)/i, 1]&.to_i
23
+ next unless content_length
24
+
25
+ raw_message = @io.read(content_length)
26
+ next unless raw_message
27
+
28
+ block.call(JSON.parse(raw_message, symbolize_names: true))
29
+ end
30
+ end
31
+ end
32
+
33
+ class MessageWriter
34
+ # @rbs @io: IO
35
+
36
+ # @rbs io: IO
37
+ # @rbs return: void
38
+ def initialize(io)
39
+ @io = io
40
+ end
41
+
42
+ # @rbs message: Hash[Symbol, untyped]
43
+ # @rbs return: void
44
+ def write(message)
45
+ message[:jsonrpc] = "2.0"
46
+ json_message = message.to_json
47
+
48
+ @io.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
49
+ @io.flush
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end