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