yoda-language-server 0.6.2 → 0.7.0

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