mui-lsp 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.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Lsp
5
+ module Protocol
6
+ # LSP Range (start and end positions)
7
+ class Range
8
+ attr_accessor :start, :end
9
+
10
+ def initialize(start:, end_pos:)
11
+ @start = start.is_a?(Position) ? start : Position.from_hash(start)
12
+ @end = end_pos.is_a?(Position) ? end_pos : Position.from_hash(end_pos)
13
+ end
14
+
15
+ def to_h
16
+ { start: @start.to_h, end: @end.to_h }
17
+ end
18
+
19
+ def self.from_hash(hash)
20
+ new(
21
+ start: hash["start"] || hash[:start],
22
+ end_pos: hash["end"] || hash[:end]
23
+ )
24
+ end
25
+
26
+ def ==(other)
27
+ return false unless other.is_a?(Range)
28
+
29
+ @start == other.start && @end == other.end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "protocol/position"
4
+ require_relative "protocol/range"
5
+ require_relative "protocol/location"
6
+ require_relative "protocol/diagnostic"
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Lsp
5
+ # Manages pending JSON-RPC requests and their callbacks
6
+ class RequestManager
7
+ def initialize
8
+ @next_id = 1
9
+ @pending_requests = {}
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def register(callback)
14
+ @mutex.synchronize do
15
+ id = @next_id
16
+ @next_id += 1
17
+ @pending_requests[id] = {
18
+ callback: callback,
19
+ registered_at: Time.now
20
+ }
21
+ id
22
+ end
23
+ end
24
+
25
+ def handle_response(id, result: nil, error: nil)
26
+ request = @mutex.synchronize { @pending_requests.delete(id) }
27
+ return false unless request
28
+
29
+ if error
30
+ request[:callback].call(nil, error)
31
+ else
32
+ request[:callback].call(result, nil)
33
+ end
34
+ true
35
+ end
36
+
37
+ def pending?(id)
38
+ @mutex.synchronize { @pending_requests.key?(id) }
39
+ end
40
+
41
+ def pending_count
42
+ @mutex.synchronize { @pending_requests.size }
43
+ end
44
+
45
+ def cancel(id)
46
+ @mutex.synchronize { !@pending_requests.delete(id).nil? }
47
+ end
48
+
49
+ def cancel_all
50
+ @mutex.synchronize { @pending_requests.clear }
51
+ end
52
+
53
+ def cleanup_stale(timeout_seconds)
54
+ now = Time.now
55
+ timed_out = []
56
+
57
+ @mutex.synchronize do
58
+ @pending_requests.each do |id, request|
59
+ timed_out << id if now - request[:registered_at] > timeout_seconds
60
+ end
61
+
62
+ timed_out.each do |id|
63
+ request = @pending_requests.delete(id)
64
+ request[:callback]&.call(nil, { "code" => -32_603, "message" => "Request timed out" })
65
+ end
66
+ end
67
+
68
+ timed_out
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Lsp
5
+ # Configuration for different LSP servers
6
+ class ServerConfig
7
+ attr_reader :name, :command, :language_ids, :file_patterns, :auto_start, :sync_on_change
8
+
9
+ def initialize(name:, command:, language_ids:, file_patterns:, auto_start: true, sync_on_change: true)
10
+ @name = name
11
+ @command = command
12
+ @language_ids = Array(language_ids)
13
+ @file_patterns = Array(file_patterns)
14
+ @auto_start = auto_start
15
+ @sync_on_change = sync_on_change
16
+ end
17
+
18
+ def handles_file?(file_path)
19
+ @file_patterns.any? do |pattern|
20
+ File.fnmatch?(pattern, file_path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
21
+ end
22
+ end
23
+
24
+ def language_id_for(file_path)
25
+ ext = File.extname(file_path).downcase
26
+ case ext
27
+ when ".rb", ".rake", ".gemspec", ".ru"
28
+ "ruby"
29
+ when ".js"
30
+ "javascript"
31
+ when ".ts"
32
+ "typescript"
33
+ when ".py"
34
+ "python"
35
+ when ".go"
36
+ "go"
37
+ when ".rs"
38
+ "rust"
39
+ when ".c"
40
+ "c"
41
+ when ".cpp", ".cc", ".cxx"
42
+ "cpp"
43
+ when ".java"
44
+ "java"
45
+ else
46
+ @language_ids.first
47
+ end
48
+ end
49
+
50
+ def to_h
51
+ {
52
+ name: @name,
53
+ command: @command,
54
+ language_ids: @language_ids,
55
+ file_patterns: @file_patterns,
56
+ auto_start: @auto_start
57
+ }
58
+ end
59
+
60
+ class << self
61
+ def solargraph(auto_start: false)
62
+ new(
63
+ name: "solargraph",
64
+ command: "solargraph stdio",
65
+ language_ids: ["ruby"],
66
+ file_patterns: ["**/*.rb", "**/*.rake", "**/Gemfile", "**/Rakefile", "**/*.gemspec"],
67
+ auto_start: auto_start
68
+ )
69
+ end
70
+
71
+ def ruby_lsp(auto_start: false, sync_on_change: false)
72
+ new(
73
+ name: "ruby-lsp",
74
+ command: "ruby-lsp",
75
+ language_ids: ["ruby"],
76
+ file_patterns: ["**/*.rb", "**/*.rake", "**/Gemfile", "**/Rakefile", "**/*.gemspec"],
77
+ auto_start: auto_start,
78
+ sync_on_change: sync_on_change
79
+ )
80
+ end
81
+
82
+ def kanayago(auto_start: false)
83
+ new(
84
+ name: "kanayago",
85
+ command: "kanayago --lsp",
86
+ language_ids: ["ruby"],
87
+ file_patterns: ["**/*.rb", "**/*.rake", "**/Gemfile", "**/Rakefile", "**/*.gemspec"],
88
+ auto_start: auto_start
89
+ )
90
+ end
91
+
92
+ def rubocop_lsp(auto_start: false)
93
+ new(
94
+ name: "rubocop",
95
+ command: "rubocop --lsp",
96
+ language_ids: ["ruby"],
97
+ file_patterns: ["**/*.rb", "**/*.rake", "**/Gemfile", "**/Rakefile", "**/*.gemspec"],
98
+ auto_start: auto_start
99
+ )
100
+ end
101
+
102
+ def custom(name:, command:, language_ids:, file_patterns:, auto_start: true, sync_on_change: true)
103
+ new(
104
+ name: name,
105
+ command: command,
106
+ language_ids: language_ids,
107
+ file_patterns: file_patterns,
108
+ auto_start: auto_start,
109
+ sync_on_change: sync_on_change
110
+ )
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Lsp
5
+ # Manages text document synchronization with LSP server
6
+ # Handles didOpen, didChange, didSave, didClose notifications
7
+ # with debouncing support for change notifications
8
+ class TextDocumentSync
9
+ DEFAULT_DEBOUNCE_MS = 300
10
+
11
+ def initialize(client:, server_config:, debounce_ms: DEFAULT_DEBOUNCE_MS)
12
+ @client = client
13
+ @server_config = server_config
14
+ @debounce_ms = debounce_ms
15
+ @open_documents = {}
16
+ @pending_changes = {}
17
+ @debounce_timers = {}
18
+ @mutex = Mutex.new
19
+ end
20
+
21
+ def did_open(uri:, text:)
22
+ file_path = uri_to_path(uri)
23
+ language_id = @server_config.language_id_for(file_path)
24
+
25
+ @mutex.synchronize do
26
+ @open_documents[uri] = {
27
+ version: 1,
28
+ text: text,
29
+ language_id: language_id
30
+ }
31
+ end
32
+
33
+ @client.did_open(
34
+ uri: uri,
35
+ language_id: language_id,
36
+ version: 1,
37
+ text: text
38
+ )
39
+ end
40
+
41
+ def did_change(uri:, text:, debounce: true)
42
+ # Skip if sync_on_change is disabled for this server
43
+ return unless @server_config.sync_on_change
44
+
45
+ @mutex.synchronize do
46
+ return unless @open_documents.key?(uri)
47
+
48
+ @open_documents[uri][:version] += 1
49
+ @open_documents[uri][:text] = text
50
+ @pending_changes[uri] = text
51
+ end
52
+
53
+ if debounce
54
+ schedule_debounced_change(uri)
55
+ else
56
+ flush_change(uri)
57
+ end
58
+ end
59
+
60
+ def did_save(uri:, text: nil)
61
+ # Flush any pending changes first
62
+ flush_change(uri)
63
+ @client.did_save(uri: uri, text: text)
64
+ end
65
+
66
+ def did_close(uri:)
67
+ cancel_debounce_timer(uri)
68
+
69
+ @mutex.synchronize do
70
+ @open_documents.delete(uri)
71
+ @pending_changes.delete(uri)
72
+ end
73
+
74
+ @client.did_close(uri: uri)
75
+ end
76
+
77
+ def open?(uri)
78
+ @mutex.synchronize { @open_documents.key?(uri) }
79
+ end
80
+
81
+ def version(uri)
82
+ @mutex.synchronize { @open_documents.dig(uri, :version) }
83
+ end
84
+
85
+ def flush_all
86
+ uris = @mutex.synchronize { @pending_changes.keys.dup }
87
+ uris.each { |uri| flush_change(uri) }
88
+ end
89
+
90
+ def close_all
91
+ uris = @mutex.synchronize { @open_documents.keys.dup }
92
+ uris.each { |uri| did_close(uri: uri) }
93
+ end
94
+
95
+ def self.path_to_uri(path)
96
+ "file://#{File.expand_path(path)}"
97
+ end
98
+
99
+ def self.uri_to_path(uri)
100
+ return nil unless uri&.start_with?("file://")
101
+
102
+ URI.decode_www_form_component(uri.sub("file://", ""))
103
+ end
104
+
105
+ private
106
+
107
+ def uri_to_path(uri)
108
+ self.class.uri_to_path(uri)
109
+ end
110
+
111
+ def schedule_debounced_change(uri)
112
+ cancel_debounce_timer(uri)
113
+
114
+ timer = Thread.new do
115
+ sleep(@debounce_ms / 1000.0)
116
+ flush_change(uri)
117
+ end
118
+
119
+ @mutex.synchronize { @debounce_timers[uri] = timer }
120
+ end
121
+
122
+ def cancel_debounce_timer(uri)
123
+ timer = @mutex.synchronize { @debounce_timers.delete(uri) }
124
+ timer&.kill
125
+ end
126
+
127
+ def flush_change(uri)
128
+ cancel_debounce_timer(uri)
129
+
130
+ doc_info = nil
131
+ text = nil
132
+
133
+ @mutex.synchronize do
134
+ text = @pending_changes.delete(uri)
135
+ doc_info = @open_documents[uri]
136
+ end
137
+
138
+ return unless text && doc_info
139
+
140
+ # Send full document sync (TextDocumentSyncKind.Full = 1)
141
+ @client.did_change(
142
+ uri: uri,
143
+ version: doc_info[:version],
144
+ changes: [{ text: text }]
145
+ )
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mui
4
+ module Lsp
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
data/lib/mui/lsp.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lsp/version"
4
+ require_relative "lsp/protocol"
5
+ require_relative "lsp/json_rpc_io"
6
+ require_relative "lsp/request_manager"
7
+ require_relative "lsp/server_config"
8
+ require_relative "lsp/client"
9
+ require_relative "lsp/text_document_sync"
10
+ require_relative "lsp/handlers"
11
+ require_relative "lsp/highlighters/diagnostic_highlighter"
12
+ require_relative "lsp/manager"
13
+ require_relative "lsp/plugin"
14
+
15
+ module Mui
16
+ module Lsp
17
+ class Error < StandardError; end
18
+ end
19
+ end
data/lib/mui_lsp.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mui/lsp"
data/sig/mui/lsp.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Mui
2
+ module Lsp
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mui-lsp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - S-H-GAMELINKS
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: mui
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ description: Provides LSP integration for Mui editor including hover, definition,
27
+ references, diagnostics, and completion
28
+ email:
29
+ - gamelinks007@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".rubocop_todo.yml"
35
+ - CHANGELOG.md
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - lib/mui/lsp.rb
40
+ - lib/mui/lsp/client.rb
41
+ - lib/mui/lsp/handlers.rb
42
+ - lib/mui/lsp/handlers/base.rb
43
+ - lib/mui/lsp/handlers/completion.rb
44
+ - lib/mui/lsp/handlers/definition.rb
45
+ - lib/mui/lsp/handlers/diagnostics.rb
46
+ - lib/mui/lsp/handlers/hover.rb
47
+ - lib/mui/lsp/handlers/references.rb
48
+ - lib/mui/lsp/highlighters/diagnostic_highlighter.rb
49
+ - lib/mui/lsp/json_rpc_io.rb
50
+ - lib/mui/lsp/manager.rb
51
+ - lib/mui/lsp/plugin.rb
52
+ - lib/mui/lsp/protocol.rb
53
+ - lib/mui/lsp/protocol/diagnostic.rb
54
+ - lib/mui/lsp/protocol/location.rb
55
+ - lib/mui/lsp/protocol/position.rb
56
+ - lib/mui/lsp/protocol/range.rb
57
+ - lib/mui/lsp/request_manager.rb
58
+ - lib/mui/lsp/server_config.rb
59
+ - lib/mui/lsp/text_document_sync.rb
60
+ - lib/mui/lsp/version.rb
61
+ - lib/mui_lsp.rb
62
+ - sig/mui/lsp.rbs
63
+ homepage: https://github.com/S-H-GAMELINKS/mui-lsp
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ allowed_push_host: https://rubygems.org
68
+ homepage_uri: https://github.com/S-H-GAMELINKS/mui-lsp
69
+ source_code_uri: https://github.com/S-H-GAMELINKS/mui-lsp
70
+ changelog_uri: https://github.com/S-H-GAMELINKS/mui-lsp/blob/main/CHANGELOG.md
71
+ rubygems_mfa_required: 'true'
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 3.2.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.6.9
87
+ specification_version: 4
88
+ summary: LSP (Language Server Protocol) plugin for Mui editor
89
+ test_files: []