yoda-language-server 0.6.2 → 0.7.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,148 @@
1
+ require 'concurrent'
2
+
3
+ module Yoda
4
+ class Server
5
+ # Handle
6
+ class LifecycleHandler
7
+ # @return [Session, nil]
8
+ attr_reader :session
9
+
10
+ # @return [Notifier]
11
+ attr_reader :notifier
12
+
13
+ def initialize(root_handler)
14
+ @root_handler = root_handler
15
+ @notifier = root_handler.notifier
16
+ end
17
+
18
+ # @param method [Symbol]
19
+ # @return [true, false]
20
+ def handle?(method)
21
+ lifecycle_handlers.key?(method)
22
+ end
23
+
24
+ # @param method [Symbol]
25
+ # @param params [Object]
26
+ def handle(method:, params:)
27
+ lifecycle_handlers[method].call(params)
28
+ end
29
+
30
+ private
31
+
32
+ def lifecycle_handlers
33
+ @lifecycle_handlers ||= {
34
+ initialize: method(:handle_initialize),
35
+ initialized: method(:handle_initialized),
36
+ shutdown: method(:handle_shutdown),
37
+ exit: method(:handle_exit),
38
+ '$/cancelRequest': method(:handle_cancel),
39
+ }
40
+ end
41
+
42
+ def handle_initialize(params)
43
+ Instrument.instance.hear(initialization_progress: method(:notify_initialization_progress)) do
44
+ @session = Session.new(params[:root_uri])
45
+ send_warnings(@session.setup || [])
46
+
47
+ LanguageServer::Protocol::Interface::InitializeResult.new(
48
+ capabilities: LanguageServer::Protocol::Interface::ServerCapabilities.new(
49
+ text_document_sync: LanguageServer::Protocol::Interface::TextDocumentSyncOptions.new(
50
+ change: LanguageServer::Protocol::Constant::TextDocumentSyncKind::FULL,
51
+ save: LanguageServer::Protocol::Interface::SaveOptions.new(
52
+ include_text: true,
53
+ ),
54
+ ),
55
+ completion_provider: LanguageServer::Protocol::Interface::CompletionOptions.new(
56
+ resolve_provider: false,
57
+ trigger_characters: ['.', '@', '[', ':', '!', '<'],
58
+ ),
59
+ hover_provider: true,
60
+ definition_provider: true,
61
+ signature_help_provider: LanguageServer::Protocol::Interface::SignatureHelpOptions.new(
62
+ trigger_characters: ['(', ','],
63
+ ),
64
+ ),
65
+ )
66
+ end
67
+ rescue => e
68
+ LanguageServer::Protocol::Interface::ResponseError.new(
69
+ message: "Failed to initialize yoda: #{e.class} #{e.message}",
70
+ code: LanguageServer::Protocol::Constant::ErrorCodes::SERVER_ERROR_START,
71
+ data: LanguageServer::Protocol::Interface::InitializeError.new(retry: false),
72
+ )
73
+ end
74
+
75
+ def handle_initialized(_params)
76
+ NO_RESPONSE
77
+ end
78
+
79
+ def handle_shutdown(_params)
80
+ end
81
+
82
+ def handle_cancel(params)
83
+ @root_handler.cancel_request(params[:id])
84
+
85
+ NO_RESPONSE
86
+ end
87
+
88
+ def handle_exit(_params)
89
+ NO_RESPONSE
90
+ end
91
+
92
+ # @param errors [Array<BaseError>]
93
+ # @return [Array<Object>]
94
+ def send_warnings(errors)
95
+ return [] if errors.empty?
96
+ gem_import_errors = errors.select { |error| error.is_a?(GemImportError) }
97
+ core_import_errors = errors.select { |error| error.is_a?(CoreImportError) }
98
+
99
+ notifier.show_message(
100
+ type: :warning,
101
+ message: "Failed to load some libraries (Please check console for details)",
102
+ )
103
+
104
+ if gem_message = gem_import_warnings(gem_import_errors)
105
+ notifier.log_message(
106
+ type: :warning,
107
+ message: gem_message,
108
+ )
109
+ end
110
+
111
+ if core_message = core_import_warnings(core_import_errors)
112
+ notifier.log_message(
113
+ type: :warning,
114
+ message: core_message,
115
+ )
116
+ end
117
+ end
118
+
119
+ # @param gem_import_errors [Array<GemImportError>]
120
+ # @return [String, nil]
121
+ def gem_import_warnings(gem_import_errors)
122
+ return if gem_import_errors.empty?
123
+ warnings = gem_import_errors.map { |error| "- #{error.name} (#{error.version})" }
124
+
125
+ <<~EOS
126
+ Failed to import some gems.
127
+ Please check these gems are installed for Ruby version #{RUBY_VERSION}.
128
+ #{warnings.join("\n")}
129
+ EOS
130
+ end
131
+
132
+ # @param gem_import_errors [Array<GemImportError>]
133
+ # @return [String, nil]
134
+ def core_import_warnings(core_import_errors)
135
+ return if core_import_errors.empty?
136
+
137
+ <<~EOS
138
+ Failed to import some core libraries (Ruby version: #{RUBY_VERSION}).
139
+ Please execute `yoda setup` with Ruby version #{RUBY_VERSION}.
140
+ EOS
141
+ end
142
+
143
+ def notify_initialization_progress(phase: nil, message: nil, **params)
144
+ notifier.event(type: :initialization, phase: phase, message: message)
145
+ end
146
+ end
147
+ end
148
+ end
@@ -1,28 +1,28 @@
1
1
  module Yoda
2
2
  class Server
3
3
  class Notifier
4
- # @param server [Server]
5
- def initialize(server)
6
- @server = server
4
+ # @param writer [ConcurrentWriter]
5
+ def initialize(writer)
6
+ @writer = writer
7
7
  end
8
8
 
9
9
  # @param type [Symbol]
10
- def busy(type:)
11
- event(type: type, phase: :begin)
10
+ def busy(type:, id: nil)
11
+ event(type: type, phase: :begin, id: id)
12
12
  yield
13
13
  ensure
14
- event(type: type, phase: :end)
14
+ event(type: type, phase: :end, id: id)
15
15
  end
16
16
 
17
17
  # @param params [Hash]
18
18
  def event(params)
19
- server.send_notification(method: 'telemetry/event', params: params)
19
+ write(method: 'telemetry/event', params: params)
20
20
  end
21
21
 
22
22
  # @param type [String, Symbol]
23
23
  # @param message [String]
24
24
  def show_message(type:, message:)
25
- server.send_notification(
25
+ write(
26
26
  method: 'window/showMessage',
27
27
  params: LanguageServer::Protocol::Interface::ShowMessageParams.new(
28
28
  type: message_type(type),
@@ -34,7 +34,7 @@ module Yoda
34
34
  # @param type [String, Symbol]
35
35
  # @param message [String]
36
36
  def log_message(type:, message:)
37
- server.send_notification(
37
+ write(
38
38
  method: 'window/logMessage',
39
39
  params: LanguageServer::Protocol::Interface::ShowMessageParams.new(
40
40
  type: message_type(type),
@@ -45,6 +45,10 @@ module Yoda
45
45
 
46
46
  private
47
47
 
48
+ def write(params)
49
+ @writer.write(params)
50
+ end
51
+
48
52
  # @param type [String, Symbol]
49
53
  def message_type(type)
50
54
  case type.to_sym
@@ -0,0 +1,41 @@
1
+ module Yoda
2
+ class Server
3
+ module Providers
4
+ require 'yoda/server/providers/base'
5
+
6
+ require 'yoda/server/providers/completion'
7
+ require 'yoda/server/providers/signature'
8
+ require 'yoda/server/providers/hover'
9
+ require 'yoda/server/providers/definition'
10
+ require 'yoda/server/providers/text_document_did_change'
11
+ require 'yoda/server/providers/text_document_did_open'
12
+ require 'yoda/server/providers/text_document_did_save'
13
+
14
+ CLASSES = [
15
+ Completion,
16
+ Definition,
17
+ Hover,
18
+ Signature,
19
+ TextDocumentDidChange,
20
+ TextDocumentDidOpen,
21
+ TextDocumentDidSave,
22
+ ].freeze
23
+
24
+ class << self
25
+ # @param method [Symbol]
26
+ # @param notifier [Notifier]
27
+ # @param session [Session]
28
+ # @return [Class<Providers::Base>, nil]
29
+ def build_provider(method:, notifier:, session:)
30
+ find_provider_class(method)&.new(notifier: notifier, session: session)
31
+ end
32
+
33
+ # @param method [Symbol]
34
+ # @return [Class<Providers::Base>, nil]
35
+ def find_provider_class(method)
36
+ CLASSES.find { |provider_class| provider_class.provide?(method) }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ module Yoda
2
+ class Server
3
+ module Providers
4
+ # @abstract
5
+ class Base
6
+ class << self
7
+ # @abstract
8
+ # @return [Symbol]
9
+ def provider_method
10
+ fail NotImplementedError
11
+ end
12
+
13
+ # @param method [Symbol]
14
+ def provide?(method)
15
+ provider_method == method
16
+ end
17
+ end
18
+
19
+ # @return [Notifier]
20
+ attr_reader :notifier
21
+
22
+ # @return [Session]
23
+ attr_reader :session
24
+
25
+ # @param notifier [Notifier]
26
+ # @param session [Notifier]
27
+ def initialize(notifier:, session:)
28
+ @notifier = notifier
29
+ @session = session
30
+ end
31
+
32
+ # @abstract
33
+ # @param params [Hash]
34
+ def provide(params)
35
+ fail NotImplementedError
36
+ end
37
+
38
+ # @return [Integer, nil] Seconds to timeout the task. if nil, the task does not timeout.
39
+ def timeout
40
+ nil
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,86 @@
1
+ module Yoda
2
+ class Server
3
+ module Providers
4
+ class Completion < Base
5
+ def self.provider_method
6
+ :'textDocument/completion'
7
+ end
8
+
9
+ def provide(params)
10
+ uri = params[:text_document][:uri]
11
+ position = params[:position]
12
+
13
+ calculate(uri, position)
14
+ end
15
+
16
+ def timeout
17
+ 10
18
+ end
19
+
20
+ private
21
+
22
+ # @param uri [String]
23
+ # @param position [{Symbol => Integer}]
24
+ def calculate(uri, position)
25
+ source = session.file_store.get(uri)
26
+ location = Parsing::Location.of_language_server_protocol_position(line: position[:line], character: position[:character])
27
+
28
+ if candidates = comment_complete(source, location)
29
+ return candidates
30
+ end
31
+ complete_from_cut_source(source, location)
32
+ end
33
+
34
+ # @param source [String]
35
+ # @param location [Parsing::Location]
36
+ # @return [LanguageServerProtocol::Interface::CompletionList, nil]
37
+ def comment_complete(source, location)
38
+ ast, comments = Parsing::Parser.new.parse_with_comments(source)
39
+ return nil unless Parsing::Query::CurrentCommentQuery.new(comments, location).current_comment
40
+ completion_worker = Evaluation::CommentCompletion.new(session.registry, ast, comments, location)
41
+ return nil unless completion_worker.available?
42
+
43
+ completion_items = completion_worker.candidates
44
+
45
+ LanguageServer::Protocol::Interface::CompletionList.new(
46
+ is_incomplete: false,
47
+ items: completion_worker.candidates.map { |completion_item| create_completion_item(completion_item) },
48
+ )
49
+ rescue ::Parser::SyntaxError
50
+ nil
51
+ end
52
+
53
+ # @param source [String]
54
+ # @param location [Parsing::Location]
55
+ # @return [LanguageServerProtocol::Interface::CompletionList, nil]
56
+ def complete_from_cut_source(source, location)
57
+ cut_source = Parsing::SourceCutter.new(source, location).error_recovered_source
58
+ method_completion_worker = Evaluation::CodeCompletion.new(session.registry, cut_source, location)
59
+ completion_items = method_completion_worker.candidates
60
+
61
+ LanguageServer::Protocol::Interface::CompletionList.new(
62
+ is_incomplete: false,
63
+ items: completion_items.map { |completion_item| create_completion_item(completion_item) },
64
+ )
65
+ end
66
+
67
+ # @param completion_item [Model::CompletionItem]
68
+ # @return [LanguageServer::Protocol::Interface::CompletionItem]
69
+ def create_completion_item(completion_item)
70
+ LanguageServer::Protocol::Interface::CompletionItem.new(
71
+ label: completion_item.description.is_a?(Model::Descriptions::FunctionDescription) ? completion_item.description.signature : completion_item.description.sort_text,
72
+ kind: completion_item.language_server_kind,
73
+ detail: completion_item.description.title,
74
+ documentation: completion_item.description.to_markdown,
75
+ sort_text: completion_item.description.sort_text,
76
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
77
+ range: LanguageServer::Protocol::Interface::Range.new(completion_item.range.to_language_server_protocol_range),
78
+ new_text: completion_item.edit_text,
79
+ ),
80
+ data: {},
81
+ )
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,44 @@
1
+ module Yoda
2
+ class Server
3
+ module Providers
4
+ class Definition < Base
5
+ def self.provider_method
6
+ :'textDocument/definition'
7
+ end
8
+
9
+ def provide(params)
10
+ calculate(params[:text_document][:uri], params[:position])
11
+ end
12
+
13
+ def timeout
14
+ 10
15
+ end
16
+
17
+ private
18
+
19
+ # @param uri [String]
20
+ # @param position [{Symbol => Integer}]
21
+ # @param include_declaration [Boolean]
22
+ def calculate(uri, position, include_declaration = false)
23
+ source = session.file_store.get(uri)
24
+ location = Parsing::Location.of_language_server_protocol_position(line: position[:line], character: position[:character])
25
+
26
+ node_worker = Evaluation::CurrentNodeExplain.new(session.registry, source, location)
27
+ references = node_worker.defined_files
28
+ references.map { |(path, line, column)| create_location(path, line, column) }
29
+ end
30
+
31
+ # @param path [String]
32
+ # @param line [Integer]
33
+ # @param column [Integer]
34
+ def create_location(path, line, column)
35
+ location = Parsing::Location.new(row: line - 1, column: column)
36
+ LanguageServer::Protocol::Interface::Location.new(
37
+ uri: session.uri_of_path(path),
38
+ range: LanguageServer::Protocol::Interface::Range.new(Parsing::Range.new(location, location).to_language_server_protocol_range),
39
+ )
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,47 @@
1
+ module Yoda
2
+ class Server
3
+ module Providers
4
+ class Hover < Base
5
+ def self.provider_method
6
+ :'textDocument/hover'
7
+ end
8
+
9
+ def provide(params)
10
+ calculate(params[:text_document][:uri], params[:position])
11
+ end
12
+
13
+ def timeout
14
+ 10
15
+ end
16
+
17
+ private
18
+
19
+ # @param uri [String]
20
+ # @param position [{Symbol => Integer}]
21
+ def calculate(uri, position)
22
+ source = session.file_store.get(uri)
23
+ location = Parsing::Location.of_language_server_protocol_position(line: position[:line], character: position[:character])
24
+
25
+ node_worker = Evaluation::CurrentNodeExplain.new(session.registry, source, location)
26
+
27
+ current_node_signature = node_worker.current_node_signature
28
+ create_hover(current_node_signature) if current_node_signature
29
+ end
30
+
31
+ # @param signature [Evaluation::NodeSignature]
32
+ def create_hover(signature)
33
+ LanguageServer::Protocol::Interface::Hover.new(
34
+ contents: signature.descriptions.map { |value| create_hover_text(value) },
35
+ range: LanguageServer::Protocol::Interface::Range.new(signature.node_range.to_language_server_protocol_range),
36
+ )
37
+ end
38
+
39
+ # @param description [Evaluation::Descriptions::Base]
40
+ # @return [String]
41
+ def create_hover_text(description)
42
+ description.to_markdown
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end