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 +7 -0
- data/.claude/commands/add-stub.md +7 -0
- data/.claude/commands/check.md +7 -0
- data/.github/workflows/ci.yml +55 -0
- data/.rspec +3 -0
- data/CLAUDE.md +57 -0
- data/README.md +147 -0
- data/Rakefile +12 -0
- data/Steepfile +5 -0
- data/exe/refined-steep-server +36 -0
- data/lib/refined/steep/server/base_server.rb +171 -0
- data/lib/refined/steep/server/io.rb +54 -0
- data/lib/refined/steep/server/lsp_server.rb +851 -0
- data/lib/refined/steep/server/message.rb +150 -0
- data/lib/refined/steep/server/steep_state.rb +80 -0
- data/lib/refined/steep/server/store.rb +113 -0
- data/lib/refined/steep/server/version.rb +9 -0
- data/lib/refined/steep/server.rb +22 -0
- data/rbs_collection.yaml +19 -0
- data/sig/external/steep.rbs +376 -0
- data/sig/refined/steep/server.rbs +8 -0
- data/spec/refined/steep/server/base_server_spec.rb +116 -0
- data/spec/refined/steep/server/io_spec.rb +54 -0
- data/spec/refined/steep/server/lsp_server_spec.rb +317 -0
- data/spec/refined/steep/server/message_spec.rb +80 -0
- data/spec/refined/steep/server/steep_state_spec.rb +46 -0
- data/spec/refined/steep/server/store_spec.rb +82 -0
- data/spec/spec_helper.rb +87 -0
- metadata +98 -0
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,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
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
|
+
[](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
data/Steepfile
ADDED
|
@@ -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
|