ruby_language_server 0.2.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/CHANGELOG.txt +67 -0
- data/FAQ_ROADMAP.md +30 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +111 -0
- data/Guardfile +36 -0
- data/LICENSE +21 -0
- data/Makefile +35 -0
- data/README.md +55 -0
- data/Rakefile +17 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/ruby_language_server +9 -0
- data/lib/ruby_language_server/code_file.rb +129 -0
- data/lib/ruby_language_server/completion.rb +87 -0
- data/lib/ruby_language_server/gem_installer.rb +24 -0
- data/lib/ruby_language_server/good_cop.rb +125 -0
- data/lib/ruby_language_server/io.rb +130 -0
- data/lib/ruby_language_server/line_context.rb +39 -0
- data/lib/ruby_language_server/location.rb +29 -0
- data/lib/ruby_language_server/logger.rb +14 -0
- data/lib/ruby_language_server/project_manager.rb +231 -0
- data/lib/ruby_language_server/scope_data/base.rb +23 -0
- data/lib/ruby_language_server/scope_data/scope.rb +104 -0
- data/lib/ruby_language_server/scope_data/variable.rb +25 -0
- data/lib/ruby_language_server/scope_parser.rb +334 -0
- data/lib/ruby_language_server/scope_parser_commands/rails_commands.rb +29 -0
- data/lib/ruby_language_server/scope_parser_commands/rake_commands.rb +31 -0
- data/lib/ruby_language_server/scope_parser_commands/readme.txt +9 -0
- data/lib/ruby_language_server/scope_parser_commands/rspec_commands.rb +26 -0
- data/lib/ruby_language_server/scope_parser_commands/ruby_commands.rb +70 -0
- data/lib/ruby_language_server/server.rb +123 -0
- data/lib/ruby_language_server/version.rb +5 -0
- data/lib/ruby_language_server.rb +14 -0
- data/ruby_language_server.gemspec +56 -0
- metadata +293 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rubocop'
|
4
|
+
|
5
|
+
module RubyLanguageServer
|
6
|
+
class GoodCop < RuboCop::Runner
|
7
|
+
CONFIG_PATH = '/project/.rubocop.yml'
|
8
|
+
FALLBACK_PATH = '/app/.rubocop.yml'
|
9
|
+
@initialization_error = nil
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
config_store = RuboCop::ConfigStore.new
|
13
|
+
config_store.options_config =
|
14
|
+
if File.exist?(CONFIG_PATH)
|
15
|
+
CONFIG_PATH
|
16
|
+
else
|
17
|
+
FALLBACK_PATH
|
18
|
+
end
|
19
|
+
super({}, config_store)
|
20
|
+
rescue Exception => exception
|
21
|
+
RubyLanguageServer.logger.error(exception)
|
22
|
+
@initialization_error = "There was an issue loading the rubocop configuration file: #{exception}. Maybe you need to add some additional gems to the ide-ruby settings?"
|
23
|
+
end
|
24
|
+
|
25
|
+
# namespace DiagnosticSeverity {
|
26
|
+
# /**
|
27
|
+
# * Reports an error.
|
28
|
+
# */
|
29
|
+
# export const Error = 1;
|
30
|
+
# /**
|
31
|
+
# * Reports a warning.
|
32
|
+
# */
|
33
|
+
# export const Warning = 2;
|
34
|
+
# /**
|
35
|
+
# * Reports an information.
|
36
|
+
# */
|
37
|
+
# export const Information = 3;
|
38
|
+
# /**
|
39
|
+
# * Reports a hint.
|
40
|
+
# */
|
41
|
+
# export const Hint = 4;
|
42
|
+
# }
|
43
|
+
|
44
|
+
# interface Diagnostic {
|
45
|
+
# /**
|
46
|
+
# * The range at which the message applies.
|
47
|
+
# */
|
48
|
+
# range: Range;
|
49
|
+
#
|
50
|
+
# /**
|
51
|
+
# * The diagnostic's severity. Can be omitted. If omitted it is up to the
|
52
|
+
# * client to interpret diagnostics as error, warning, info or hint.
|
53
|
+
# */
|
54
|
+
# severity?: number;
|
55
|
+
#
|
56
|
+
# /**
|
57
|
+
# * The diagnostic's code. Can be omitted.
|
58
|
+
# */
|
59
|
+
# code?: number | string;
|
60
|
+
#
|
61
|
+
# /**
|
62
|
+
# * A human-readable string describing the source of this
|
63
|
+
# * diagnostic, e.g. 'typescript' or 'super lint'.
|
64
|
+
# */
|
65
|
+
# source?: string;
|
66
|
+
#
|
67
|
+
# /**
|
68
|
+
# * The diagnostic's message.
|
69
|
+
# */
|
70
|
+
# message: string;
|
71
|
+
# }
|
72
|
+
|
73
|
+
def diagnostic_severity_for(severity)
|
74
|
+
case severity.to_s
|
75
|
+
when 'error', 'fatal'
|
76
|
+
1
|
77
|
+
when 'warning'
|
78
|
+
2
|
79
|
+
when 'refactor', 'convention'
|
80
|
+
3
|
81
|
+
else
|
82
|
+
RubyLanguageServer.logger.warn("Could not map severity for #{severity} - returning 2")
|
83
|
+
2
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def diagnostics(text, filename = nil)
|
88
|
+
return initialization_offenses unless @initialization_error.nil?
|
89
|
+
|
90
|
+
maximum_severity = (ENV['LINT_LEVEL'] || 4).to_i
|
91
|
+
enabled_offenses = offenses(text, filename).reject { |offense| offense.status == :disabled }
|
92
|
+
enabled_offenses.map do |offense|
|
93
|
+
{
|
94
|
+
range: Location.position_hash(offense.location.line, offense.location.column, offense.location.last_line, offense.location.last_column),
|
95
|
+
severity: diagnostic_severity_for(offense.severity),
|
96
|
+
# code?: number | string;
|
97
|
+
code: 'code',
|
98
|
+
source: "RuboCop:#{offense.cop_name}",
|
99
|
+
message: offense.message
|
100
|
+
}
|
101
|
+
end.select { |hash| hash[:severity] <= maximum_severity }
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def offenses(text, filename)
|
107
|
+
processed_source = RuboCop::ProcessedSource.new(text, 2.4, filename)
|
108
|
+
offenses = inspect_file(processed_source)
|
109
|
+
offenses.compact.flatten
|
110
|
+
end
|
111
|
+
|
112
|
+
def initialization_offenses
|
113
|
+
[
|
114
|
+
{
|
115
|
+
range: Location.position_hash(1, 1, 1, 1),
|
116
|
+
severity: 'startup', # diagnostic_severity_for(offense.severity),
|
117
|
+
# code?: number | string;
|
118
|
+
code: 'code',
|
119
|
+
source: 'RuboCop:RubyLanguageServer',
|
120
|
+
message: @initialization_error
|
121
|
+
}
|
122
|
+
]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module RubyLanguageServer
|
6
|
+
class IO
|
7
|
+
def initialize(server)
|
8
|
+
@server = server
|
9
|
+
server.io = self
|
10
|
+
loop do
|
11
|
+
(id, response) = process_request(STDIN)
|
12
|
+
return_response(id, response, STDOUT) unless id.nil?
|
13
|
+
rescue SignalException => e
|
14
|
+
RubyLanguageServer.logger.error "We received a signal. Let's bail: #{e}"
|
15
|
+
exit(true)
|
16
|
+
rescue Exception => e
|
17
|
+
RubyLanguageServer.logger.error "Something when horribly wrong: #{e}"
|
18
|
+
backtrace = e.backtrace * "\n"
|
19
|
+
RubyLanguageServer.logger.error "Backtrace:\n#{backtrace}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def return_response(id, response, io = STDOUT)
|
24
|
+
full_response = {
|
25
|
+
jsonrpc: '2.0',
|
26
|
+
id: id,
|
27
|
+
result: response
|
28
|
+
}
|
29
|
+
response_body = JSON.unparse(full_response)
|
30
|
+
# RubyLanguageServer.logger.debug "response_body: #{response_body}"
|
31
|
+
io.write "Content-Length: #{response_body.length + 0}\r\n"
|
32
|
+
io.write "\r\n"
|
33
|
+
io.write response_body
|
34
|
+
io.flush
|
35
|
+
end
|
36
|
+
|
37
|
+
def send_notification(message, params, io = STDOUT)
|
38
|
+
full_response = {
|
39
|
+
jsonrpc: '2.0',
|
40
|
+
method: message,
|
41
|
+
params: params
|
42
|
+
}
|
43
|
+
body = JSON.unparse(full_response)
|
44
|
+
# RubyLanguageServer.logger.debug "body: #{body}"
|
45
|
+
io.write "Content-Length: #{body.length + 0}\r\n"
|
46
|
+
io.write "\r\n"
|
47
|
+
io.write body
|
48
|
+
io.flush
|
49
|
+
end
|
50
|
+
|
51
|
+
def process_request(io = STDIN)
|
52
|
+
request_body = get_request(io)
|
53
|
+
# RubyLanguageServer.logger.debug "request_body: #{request_body}"
|
54
|
+
request_json = JSON.parse request_body
|
55
|
+
id = request_json['id']
|
56
|
+
method_name = request_json['method']
|
57
|
+
params = request_json['params']
|
58
|
+
method_name = "on_#{method_name.gsub(/[^\w]/, '_')}"
|
59
|
+
if @server.respond_to? method_name
|
60
|
+
response = @server.send(method_name, params)
|
61
|
+
exit(true) if response == 'EXIT'
|
62
|
+
return id, response
|
63
|
+
else
|
64
|
+
RubyLanguageServer.logger.warn "SERVER DOES NOT RESPOND TO #{method_name}"
|
65
|
+
return nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_request(io = STDIN)
|
70
|
+
initial_line = get_initial_request_line(io)
|
71
|
+
RubyLanguageServer.logger.debug "initial_line: #{initial_line}"
|
72
|
+
length = get_length(initial_line)
|
73
|
+
content = ''
|
74
|
+
while content.length < length + 2
|
75
|
+
begin
|
76
|
+
content += get_content(length + 2, io) # Why + 2? CRLF?
|
77
|
+
rescue Exception => exception
|
78
|
+
RubyLanguageServer.logger.error exception
|
79
|
+
# We have almost certainly been disconnected from the server
|
80
|
+
exit!(1)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
RubyLanguageServer.logger.debug "content.length: #{content.length}"
|
84
|
+
content
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_initial_request_line(io = STDIN)
|
88
|
+
io.gets
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_length(string)
|
92
|
+
return 0 if string.nil?
|
93
|
+
|
94
|
+
string.match(/Content-Length: (\d+)/)[1].to_i
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_content(size, io = STDIN)
|
98
|
+
io.read(size)
|
99
|
+
end
|
100
|
+
|
101
|
+
# http://www.alecjacobson.com/weblog/?p=75
|
102
|
+
# def stdin_read_char
|
103
|
+
# begin
|
104
|
+
# # save previous state of stty
|
105
|
+
# old_state = `stty -g`
|
106
|
+
# # disable echoing and enable raw (not having to press enter)
|
107
|
+
# system "stty raw -echo"
|
108
|
+
# c = STDIN.getc.chr
|
109
|
+
# # gather next two characters of special keys
|
110
|
+
# if(c=="\e")
|
111
|
+
# extra_thread = Thread.new{
|
112
|
+
# c = c + STDIN.getc.chr
|
113
|
+
# c = c + STDIN.getc.chr
|
114
|
+
# }
|
115
|
+
# # wait just long enough for special keys to get swallowed
|
116
|
+
# extra_thread.join(0.00001)
|
117
|
+
# # kill thread so not-so-long special keys don't wait on getc
|
118
|
+
# extra_thread.kill
|
119
|
+
# end
|
120
|
+
# rescue Exception => ex
|
121
|
+
# puts "#{ex.class}: #{ex.message}"
|
122
|
+
# puts ex.backtrace
|
123
|
+
# ensure
|
124
|
+
# # restore previous state of stty
|
125
|
+
# system "stty #{old_state}"
|
126
|
+
# end
|
127
|
+
# return c
|
128
|
+
# end
|
129
|
+
end # class
|
130
|
+
end # module
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The purpose of this class is to parse a single line and return an array of the
|
4
|
+
# context words given the position of the cursor.
|
5
|
+
|
6
|
+
# FooModule::BarModule.method_name.ivar = some_var.some_method
|
7
|
+
# ^
|
8
|
+
# ['FooModule', 'BarModule', 'method_name']
|
9
|
+
# ^
|
10
|
+
# ['some_var']
|
11
|
+
|
12
|
+
# If the first part of the context is :: then it returns a leading nil in the array
|
13
|
+
|
14
|
+
# ::FooModule
|
15
|
+
# ^
|
16
|
+
# [nil, 'FooModule']
|
17
|
+
|
18
|
+
module RubyLanguageServer
|
19
|
+
module LineContext
|
20
|
+
def self.for(line, position)
|
21
|
+
# Grab just the last part of the line - from the index onward
|
22
|
+
line_end = line[position..-1]
|
23
|
+
return nil if line_end.nil?
|
24
|
+
|
25
|
+
# Grab the portion of the word that starts at the position toward the end of the line
|
26
|
+
match = line_end.partition(/^(@{0,2}\w+)/)[1]
|
27
|
+
RubyLanguageServer.logger.debug("match: #{match}")
|
28
|
+
# Get the start of the line to the end of the matched word
|
29
|
+
line_start = line[0..(position + match.length - 1)]
|
30
|
+
RubyLanguageServer.logger.debug("line_start: #{line_start}")
|
31
|
+
# Match as much as we can to the end of the line - which is now the end of the word
|
32
|
+
end_match = line_start.partition(/(@{0,2}[:\.\w]+)$/)[1]
|
33
|
+
matches = end_match.split('.', -1)
|
34
|
+
matches = matches.map { |m| m.length.positive? ? m.split('::', -1) : m }.flatten
|
35
|
+
RubyLanguageServer.logger.debug("matches: #{matches}")
|
36
|
+
matches
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLanguageServer
|
4
|
+
# Hash factories for the language server
|
5
|
+
module Location
|
6
|
+
def self.hash(uri, start_line, start_character = 1, end_line = nil, end_character = nil)
|
7
|
+
{
|
8
|
+
uri: uri,
|
9
|
+
range: position_hash(start_line, start_character, end_line, end_character)
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.position_hash(start_line, start_character = 1, end_line = nil, end_character = nil)
|
14
|
+
end_line ||= start_line
|
15
|
+
end_character ||= start_character
|
16
|
+
{
|
17
|
+
start:
|
18
|
+
{
|
19
|
+
line: start_line - 1,
|
20
|
+
character: start_character
|
21
|
+
},
|
22
|
+
'end': {
|
23
|
+
line: end_line - 1,
|
24
|
+
character: end_character
|
25
|
+
}
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module RubyLanguageServer
|
6
|
+
level_name = ENV.fetch('LOG_LEVEL') { 'info' }.upcase
|
7
|
+
# level_name = 'DEBUG'
|
8
|
+
level = Logger::Severity.const_get(level_name)
|
9
|
+
@logger = ::Logger.new(STDERR, level: level)
|
10
|
+
|
11
|
+
def self.logger
|
12
|
+
@logger
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,231 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fuzzy_match'
|
4
|
+
require 'amatch' # note that you have to require this... fuzzy_match won't require it for you
|
5
|
+
FuzzyMatch.engine = :amatch # This should be in a config somewhere
|
6
|
+
|
7
|
+
module RubyLanguageServer
|
8
|
+
class ProjectManager
|
9
|
+
attr_reader :uri_code_file_hash
|
10
|
+
|
11
|
+
def initialize(uri)
|
12
|
+
@root_path = uri
|
13
|
+
@root_uri = "file://#{@root_path}"
|
14
|
+
# This is {uri: code_file} where content stuff is like
|
15
|
+
@uri_code_file_hash = {}
|
16
|
+
@update_mutext = Mutex.new
|
17
|
+
|
18
|
+
@additional_gems_installed = false
|
19
|
+
@additional_gem_mutex = Mutex.new
|
20
|
+
|
21
|
+
scan_all_project_files
|
22
|
+
end
|
23
|
+
|
24
|
+
def diagnostics_ready?
|
25
|
+
@additional_gem_mutex.synchronize { @additional_gems_installed }
|
26
|
+
end
|
27
|
+
|
28
|
+
def install_additional_gems(gem_names)
|
29
|
+
Thread.new do
|
30
|
+
RubyLanguageServer::GemInstaller.install_gems(gem_names)
|
31
|
+
@additional_gem_mutex.synchronize { @additional_gems_installed = true }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def text_for_uri(uri)
|
36
|
+
code_file = code_file_for_uri(uri)
|
37
|
+
code_file&.text || ''
|
38
|
+
end
|
39
|
+
|
40
|
+
def code_file_for_uri(uri, text = nil)
|
41
|
+
code_file = @uri_code_file_hash[uri]
|
42
|
+
code_file = @uri_code_file_hash[uri] = CodeFile.new(uri, text) if code_file.nil?
|
43
|
+
code_file
|
44
|
+
end
|
45
|
+
|
46
|
+
def tags_for_uri(uri)
|
47
|
+
code_file = code_file_for_uri(uri)
|
48
|
+
return {} if code_file.nil?
|
49
|
+
|
50
|
+
code_file.tags
|
51
|
+
end
|
52
|
+
|
53
|
+
def root_scope_for(uri)
|
54
|
+
code_file = code_file_for_uri(uri)
|
55
|
+
RubyLanguageServer.logger.error('code_file.nil?!!!!!!!!!!!!!!') if code_file.nil?
|
56
|
+
code_file&.root_scope
|
57
|
+
end
|
58
|
+
|
59
|
+
def all_scopes
|
60
|
+
@uri_code_file_hash.values.map(&:root_scope).map(&:self_and_descendants).flatten
|
61
|
+
end
|
62
|
+
|
63
|
+
# Return the list of scopes [deepest, parent, ..., Object]
|
64
|
+
def scopes_at(uri, position)
|
65
|
+
root_scope = root_scope_for(uri)
|
66
|
+
root_scope.scopes_at(position)
|
67
|
+
end
|
68
|
+
|
69
|
+
def completion_at(uri, position)
|
70
|
+
relative_position = position.dup
|
71
|
+
relative_position.character = relative_position.character # To get before the . or ::
|
72
|
+
# RubyLanguageServer.logger.debug("relative_position #{relative_position}")
|
73
|
+
RubyLanguageServer.logger.debug("scopes_at(uri, position) #{scopes_at(uri, position).map(&:name)}")
|
74
|
+
context_scope = scopes_at(uri, position).first || root_scope_for(uri)
|
75
|
+
context = context_at_location(uri, relative_position)
|
76
|
+
return {} if context.nil? || context == ''
|
77
|
+
|
78
|
+
RubyLanguageServer::Completion.completion(context, context_scope, all_scopes)
|
79
|
+
end
|
80
|
+
|
81
|
+
# interface CompletionItem {
|
82
|
+
# /**
|
83
|
+
# * The label of this completion item. By default
|
84
|
+
# * also the text that is inserted when selecting
|
85
|
+
# * this completion.
|
86
|
+
# */
|
87
|
+
# label: string;
|
88
|
+
# /**
|
89
|
+
# * The kind of this completion item. Based of the kind
|
90
|
+
# * an icon is chosen by the editor.
|
91
|
+
# */
|
92
|
+
# kind?: number;
|
93
|
+
# /**
|
94
|
+
# * A human-readable string with additional information
|
95
|
+
# * about this item, like type or symbol information.
|
96
|
+
# */
|
97
|
+
# detail?: string;
|
98
|
+
# /**
|
99
|
+
# * A human-readable string that represents a doc-comment.
|
100
|
+
# */
|
101
|
+
# documentation?: string;
|
102
|
+
# /**
|
103
|
+
# * A string that shoud be used when comparing this item
|
104
|
+
# * with other items. When `falsy` the label is used.
|
105
|
+
# */
|
106
|
+
# sortText?: string;
|
107
|
+
# /**
|
108
|
+
# * A string that should be used when filtering a set of
|
109
|
+
# * completion items. When `falsy` the label is used.
|
110
|
+
# */
|
111
|
+
# filterText?: string;
|
112
|
+
# /**
|
113
|
+
# * A string that should be inserted a document when selecting
|
114
|
+
# * this completion. When `falsy` the label is used.
|
115
|
+
# */
|
116
|
+
# insertText?: string;
|
117
|
+
# /**
|
118
|
+
# * The format of the insert text. The format applies to both the `insertText` property
|
119
|
+
# * and the `newText` property of a provided `textEdit`.
|
120
|
+
# */
|
121
|
+
# insertTextFormat?: InsertTextFormat;
|
122
|
+
# /**
|
123
|
+
# * An edit which is applied to a document when selecting this completion. When an edit is provided the value of
|
124
|
+
# * `insertText` is ignored.
|
125
|
+
# *
|
126
|
+
# * *Note:* The range of the edit must be a single line range and it must contain the position at which completion
|
127
|
+
# * has been requested.
|
128
|
+
# */
|
129
|
+
# textEdit?: TextEdit;
|
130
|
+
# /**
|
131
|
+
# * An optional array of additional text edits that are applied when
|
132
|
+
# * selecting this completion. Edits must not overlap with the main edit
|
133
|
+
# * nor with themselves.
|
134
|
+
# */
|
135
|
+
# additionalTextEdits?: TextEdit[];
|
136
|
+
# /**
|
137
|
+
# * An optional set of characters that when pressed while this completion is active will accept it first and
|
138
|
+
# * then type that character. *Note* that all commit characters should have `length=1` and that superfluous
|
139
|
+
# * characters will be ignored.
|
140
|
+
# */
|
141
|
+
# commitCharacters?: string[];
|
142
|
+
# /**
|
143
|
+
# * An optional command that is executed *after* inserting this completion. *Note* that
|
144
|
+
# * additional modifications to the current document should be described with the
|
145
|
+
# * additionalTextEdits-property.
|
146
|
+
# */
|
147
|
+
# command?: Command;
|
148
|
+
# /**
|
149
|
+
# * An data entry field that is preserved on a completion item between
|
150
|
+
# * a completion and a completion resolve request.
|
151
|
+
# */
|
152
|
+
# data?: any
|
153
|
+
# }
|
154
|
+
|
155
|
+
def scan_all_project_files
|
156
|
+
project_ruby_files = Dir.glob('/project/**/*.rb')
|
157
|
+
RubyLanguageServer.logger.debug("scan_all_project_files: #{project_ruby_files * ','}")
|
158
|
+
Thread.new do
|
159
|
+
project_ruby_files.each do |container_path|
|
160
|
+
text = File.read(container_path)
|
161
|
+
relative_path = container_path.delete_prefix('/project/')
|
162
|
+
host_uri = @root_uri + relative_path
|
163
|
+
update_document_content(host_uri, text)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def update_document_content(uri, text)
|
169
|
+
@update_mutext.synchronize do
|
170
|
+
RubyLanguageServer.logger.debug("update_document_content: #{uri}")
|
171
|
+
# RubyLanguageServer.logger.error("@root_path: #{@root_path}")
|
172
|
+
code_file = code_file_for_uri(uri, text)
|
173
|
+
code_file.text = text
|
174
|
+
diagnostics_ready? ? code_file.diagnostics : []
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def context_at_location(uri, position)
|
179
|
+
lines = text_for_uri(uri).split("\n")
|
180
|
+
line = lines[position.line]
|
181
|
+
return [] if line.nil? || line.strip.length.zero?
|
182
|
+
|
183
|
+
contexts = LineContext.for(line, position.character)
|
184
|
+
RubyLanguageServer.logger.debug("LineContext.for(line, position.character): #{contexts}")
|
185
|
+
contexts
|
186
|
+
end
|
187
|
+
|
188
|
+
def word_at_location(uri, position)
|
189
|
+
context_at_location(uri, position).last
|
190
|
+
end
|
191
|
+
|
192
|
+
def possible_definitions(uri, position)
|
193
|
+
name = word_at_location(uri, position)
|
194
|
+
return {} if name == ''
|
195
|
+
|
196
|
+
name = 'initialize' if name == 'new'
|
197
|
+
scope = scopes_at(uri, position).first
|
198
|
+
results = scope_definitions_for(name, scope, uri)
|
199
|
+
return results unless results.empty?
|
200
|
+
|
201
|
+
project_definitions_for(name, scope)
|
202
|
+
end
|
203
|
+
|
204
|
+
def scope_definitions_for(name, scope, uri)
|
205
|
+
check_scope = scope
|
206
|
+
return_array = []
|
207
|
+
while check_scope
|
208
|
+
scope.variables.each do |variable|
|
209
|
+
return_array << Location.hash(uri, variable.line) if variable.name == name
|
210
|
+
end
|
211
|
+
check_scope = check_scope.parent
|
212
|
+
end
|
213
|
+
RubyLanguageServer.logger.debug("scope_definitions_for(#{name}, #{scope}, #{uri}: #{return_array.uniq})")
|
214
|
+
return_array.uniq
|
215
|
+
end
|
216
|
+
|
217
|
+
def project_definitions_for(name, _scope)
|
218
|
+
return_array = @uri_code_file_hash.keys.each_with_object([]) do |uri, ary|
|
219
|
+
tags = tags_for_uri(uri)
|
220
|
+
RubyLanguageServer.logger.debug("tags_for_uri(#{uri}): #{tags_for_uri(uri)}")
|
221
|
+
next if tags.nil?
|
222
|
+
|
223
|
+
match_tags = tags.select { |tag| tag[:name] == name }
|
224
|
+
match_tags.each do |tag|
|
225
|
+
ary << Location.hash(uri, tag[:location][:range][:start][:line] + 1)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
return_array
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLanguageServer
|
4
|
+
module ScopeData
|
5
|
+
class Base
|
6
|
+
TYPE_MODULE = :module
|
7
|
+
TYPE_CLASS = :class
|
8
|
+
TYPE_METHOD = :method
|
9
|
+
TYPE_BLOCK = :block
|
10
|
+
TYPE_ROOT = :root
|
11
|
+
TYPE_VARIABLE = :variable
|
12
|
+
|
13
|
+
JoinHash = {
|
14
|
+
TYPE_MODULE => '::',
|
15
|
+
TYPE_CLASS => '::',
|
16
|
+
TYPE_METHOD => '#',
|
17
|
+
TYPE_BLOCK => '>',
|
18
|
+
TYPE_ROOT => '',
|
19
|
+
TYPE_VARIABLE => '^'
|
20
|
+
}.freeze
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLanguageServer
|
4
|
+
module ScopeData
|
5
|
+
# The Scope class is basically a container with context.
|
6
|
+
# It is used to track top & bottom line, variables in this scope, contanst, and children - which could be functions, classes, blocks, etc. Anything that adds scope.
|
7
|
+
# Remember, this is scope for a file. It seems reasonabble that this will get used less in the future when we know more about classes.
|
8
|
+
class Scope < Base
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
attr_accessor :top_line # first line
|
12
|
+
attr_accessor :bottom_line # last line
|
13
|
+
attr_accessor :depth # how many parent scopes
|
14
|
+
attr_accessor :parent # parent scope
|
15
|
+
attr_accessor :variables # variables declared in this scope
|
16
|
+
attr_accessor :constants # constants declared in this scope
|
17
|
+
attr_accessor :children # child scopes
|
18
|
+
attr_accessor :type # Type of this scope (module, class, block)
|
19
|
+
attr_accessor :name # method
|
20
|
+
attr_accessor :superclass_name # superclass name
|
21
|
+
|
22
|
+
def initialize(parent = nil, type = TYPE_ROOT, name = '', top_line = 1, _column = 1)
|
23
|
+
super()
|
24
|
+
@parent = parent
|
25
|
+
@type = type
|
26
|
+
@name = name
|
27
|
+
@top_line = top_line
|
28
|
+
@depth = parent.nil? ? 0 : parent.depth + 1
|
29
|
+
@full_name = [parent ? parent.full_name : nil, @name].compact.join(JoinHash[type]) unless type == TYPE_ROOT
|
30
|
+
@children = []
|
31
|
+
@variables = []
|
32
|
+
@constants = []
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"Scope: #{@name} (#{@full_name}) #{@top_line}-#{@bottom_line} children: #{@children} vars: #{@variables}"
|
37
|
+
end
|
38
|
+
|
39
|
+
def pretty_print(pp) # rubocop:disable Naming/UncommunicativeMethodParamName
|
40
|
+
{
|
41
|
+
Scope: {
|
42
|
+
type: type,
|
43
|
+
name: name,
|
44
|
+
lines: [@top_line, @bottom_line],
|
45
|
+
children: children,
|
46
|
+
variables: variables
|
47
|
+
}
|
48
|
+
}.pretty_print(pp)
|
49
|
+
end
|
50
|
+
|
51
|
+
def full_name
|
52
|
+
@full_name || @name
|
53
|
+
end
|
54
|
+
|
55
|
+
def has_variable_or_constant?(variable) # rubocop:disable Naming/PredicateName
|
56
|
+
test_array = variable.constant? ? constants : variables
|
57
|
+
matching_variable = test_array.detect { |test_variable| (test_variable.name == variable.name) }
|
58
|
+
!matching_variable.nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
# Return the deepest child scopes of this scope - and on up.
|
62
|
+
# Not done recuresively because we don't really need to.
|
63
|
+
# Normally called on a root scope.
|
64
|
+
def scopes_at(position)
|
65
|
+
line = position.line
|
66
|
+
matching_scopes = select do |scope|
|
67
|
+
scope.top_line && scope.bottom_line && (scope.top_line..scope.bottom_line).cover?(line)
|
68
|
+
end
|
69
|
+
return [] if matching_scopes == []
|
70
|
+
|
71
|
+
deepest_scope = matching_scopes.max_by(&:depth)
|
72
|
+
deepest_scope.self_and_ancestors
|
73
|
+
end
|
74
|
+
|
75
|
+
def each
|
76
|
+
self_and_descendants.each { |member| yield member }
|
77
|
+
end
|
78
|
+
|
79
|
+
# Self and all descendents flattened into array
|
80
|
+
def self_and_descendants
|
81
|
+
[self] + descendants
|
82
|
+
end
|
83
|
+
|
84
|
+
def descendants
|
85
|
+
children.map(&:self_and_descendants).flatten
|
86
|
+
end
|
87
|
+
|
88
|
+
# [self, parent, parent.parent...]
|
89
|
+
def self_and_ancestors
|
90
|
+
return [self, parent.self_and_ancestors].flatten unless parent.nil?
|
91
|
+
|
92
|
+
[self]
|
93
|
+
end
|
94
|
+
|
95
|
+
def set_superclass_name(partial)
|
96
|
+
if partial.start_with?('::')
|
97
|
+
@superclass_name = partial.gsub(/^::/, '')
|
98
|
+
else
|
99
|
+
@superclass_name = [parent ? parent.full_name : nil, partial].compact.join(JoinHash[type])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|