solargraph 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.
- checksums.yaml +7 -0
- data/bin/solargraph +6 -0
- data/lib/solargraph.rb +39 -0
- data/lib/solargraph/api_map.rb +538 -0
- data/lib/solargraph/code_map.rb +404 -0
- data/lib/solargraph/live_parser.rb +265 -0
- data/lib/solargraph/mapper.rb +31 -0
- data/lib/solargraph/node_methods.rb +48 -0
- data/lib/solargraph/server.rb +21 -0
- data/lib/solargraph/shell.rb +84 -0
- data/lib/solargraph/snippets.rb +186 -0
- data/lib/solargraph/suggestion.rb +40 -0
- data/lib/solargraph/version.rb +3 -0
- data/lib/solargraph/yard_map.rb +119 -0
- data/yardoc/2.0.0.tar.gz +0 -0
- metadata +160 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module Solargraph
|
2
|
+
class Mapper
|
3
|
+
def initialize
|
4
|
+
@default_api_map = Solargraph::ApiMap.new
|
5
|
+
stub = Parser::CurrentRuby.parse(Solargraph::LiveParser.parse(nil))
|
6
|
+
@default_api_map.merge(stub)
|
7
|
+
@default_api_map.freeze
|
8
|
+
@require_nodes = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def get filename, text
|
12
|
+
workspace = find_workspace(filename)
|
13
|
+
CodeMap.new(text, api_map: @default_api_map, workspace: workspace, require_nodes: @require_nodes)
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_workspace filename
|
17
|
+
dirname = filename
|
18
|
+
lastname = nil
|
19
|
+
result = nil
|
20
|
+
until dirname == lastname
|
21
|
+
if File.file?("#{dirname}/Gemfile")
|
22
|
+
result = dirname
|
23
|
+
break
|
24
|
+
end
|
25
|
+
lastname = dirname
|
26
|
+
dirname = File.dirname(dirname)
|
27
|
+
end
|
28
|
+
result || File.dirname(filename)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Solargraph
|
2
|
+
module NodeMethods
|
3
|
+
def unpack_name(node)
|
4
|
+
pack_name(node).join("::")
|
5
|
+
end
|
6
|
+
|
7
|
+
def pack_name(node)
|
8
|
+
parts = []
|
9
|
+
node.children.each { |n|
|
10
|
+
if n.kind_of?(AST::Node)
|
11
|
+
if n.type == :cbase
|
12
|
+
parts = pack_name(n)
|
13
|
+
else
|
14
|
+
parts += pack_name(n)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
parts.push n unless n.nil?
|
18
|
+
end
|
19
|
+
}
|
20
|
+
parts
|
21
|
+
end
|
22
|
+
|
23
|
+
def infer node
|
24
|
+
if node.type == :str
|
25
|
+
return 'String'
|
26
|
+
elsif node.type == :array
|
27
|
+
return 'Array'
|
28
|
+
elsif node.type == :hash
|
29
|
+
return 'Hash'
|
30
|
+
elsif node.type == :int
|
31
|
+
return 'Integer'
|
32
|
+
elsif node.type == :float
|
33
|
+
return 'Float'
|
34
|
+
elsif node.type == :send
|
35
|
+
if node.children[0].nil?
|
36
|
+
# TODO Another local variable or method or something? sheesh
|
37
|
+
else
|
38
|
+
ns = unpack_name(node.children[0])
|
39
|
+
if node.children[1] == :new
|
40
|
+
return ns
|
41
|
+
end
|
42
|
+
end
|
43
|
+
elsif node.type == :cbase or node.type == :const
|
44
|
+
unpack_name node
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module Solargraph
|
4
|
+
class Server < Sinatra::Base
|
5
|
+
set :port, 0
|
6
|
+
|
7
|
+
post '/suggest' do
|
8
|
+
content_type :json
|
9
|
+
begin
|
10
|
+
code_map = CodeMap.new(code: params['text'], filename: params['filename'])
|
11
|
+
offset = code_map.get_offset(params['line'].to_i, params['col'].to_i)
|
12
|
+
sugg = code_map.suggest_at(offset, with_snippets: true, filtered: true)
|
13
|
+
{ "status" => "ok", "suggestions" => sugg }.to_json
|
14
|
+
rescue Exception => e
|
15
|
+
STDERR.puts e
|
16
|
+
STDERR.puts e.backtrace.join("\n")
|
17
|
+
{ "status" => "err", "message" => e.message }.to_json
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'json'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'rubygems/package'
|
5
|
+
require 'zlib'
|
6
|
+
|
7
|
+
module Solargraph
|
8
|
+
class Shell < Thor
|
9
|
+
desc 'prepare', 'Cache YARD files for the current environment'
|
10
|
+
option :force, type: :boolean, aliases: :f, desc: 'Force download of YARDOC files if they already exist'
|
11
|
+
option :host, type: :string, aliases: :h, desc: 'The host that provides YARDOC files for download', default: 'yardoc.solargraph.org'
|
12
|
+
def prepare
|
13
|
+
cache_dir = File.join(Dir.home, '.solargraph', 'cache')
|
14
|
+
version_dir = File.join(cache_dir, '2.0.0')
|
15
|
+
unless File.exist?(version_dir) or options[:force]
|
16
|
+
FileUtils.mkdir_p cache_dir
|
17
|
+
require 'net/http'
|
18
|
+
puts 'Downloading 2.0.0...'
|
19
|
+
Net::HTTP.start(options[:host]) do |http|
|
20
|
+
resp = http.get("/2.0.0.tar.gz")
|
21
|
+
open(File.join(cache_dir, '2.0.0.tar.gz'), "wb") do |file|
|
22
|
+
file.write(resp.body)
|
23
|
+
end
|
24
|
+
puts 'Uncompressing archives...'
|
25
|
+
FileUtils.rm_rf version_dir if File.exist?(version_dir)
|
26
|
+
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open(File.join(cache_dir, '2.0.0.tar.gz')))
|
27
|
+
tar_extract.rewind
|
28
|
+
tar_extract.each do |entry|
|
29
|
+
if entry.directory?
|
30
|
+
FileUtils.mkdir_p File.join(cache_dir, entry.full_name)
|
31
|
+
else
|
32
|
+
FileUtils.mkdir_p File.join(cache_dir, File.dirname(entry.full_name))
|
33
|
+
File.open(File.join(cache_dir, entry.full_name), 'wb') do |f|
|
34
|
+
f << entry.read
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
tar_extract.close
|
39
|
+
FileUtils.rm File.join(cache_dir, '2.0.0.tar.gz')
|
40
|
+
puts 'Done.'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
desc 'bundled', 'Get a list of bundled gems'
|
46
|
+
def bundled
|
47
|
+
Bundler.load.specs.each { |s|
|
48
|
+
puts s.name
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
desc 'server', 'Start a Solargraph server'
|
53
|
+
def server
|
54
|
+
Solargraph::Server.run!
|
55
|
+
end
|
56
|
+
|
57
|
+
desc 'suggest', 'Get code suggestions for the provided input'
|
58
|
+
long_desc <<-LONGDESC
|
59
|
+
Analyze a Ruby file and output a list of code suggestions in JSON format.
|
60
|
+
LONGDESC
|
61
|
+
option :line, type: :numeric, aliases: :l, desc: 'Zero-based line number', required: true
|
62
|
+
option :column, type: :numeric, aliases: [:c, :col], desc: 'Zero-based column number', required: true
|
63
|
+
option :filename, type: :string, aliases: :f, desc: 'File name', required: false
|
64
|
+
def suggest(*filenames)
|
65
|
+
# HACK: The ARGV array needs to be manipulated for ARGF.read to work
|
66
|
+
ARGV.clear
|
67
|
+
ARGV.concat filenames
|
68
|
+
text = ARGF.read
|
69
|
+
filename = options[:filename] || filenames[0]
|
70
|
+
begin
|
71
|
+
code_map = CodeMap.new(code: text, filename: filename)
|
72
|
+
offset = code_map.get_offset(options[:line], options[:column])
|
73
|
+
sugg = code_map.suggest_at(offset, with_snippets: true, filtered: true)
|
74
|
+
result = { "status" => "ok", "suggestions" => sugg }.to_json
|
75
|
+
STDOUT.puts result
|
76
|
+
rescue Exception => e
|
77
|
+
STDERR.puts e
|
78
|
+
STDERR.puts e.backtrace.join("\n")
|
79
|
+
result = { "status" => "err", "message" => e.message }.to_json
|
80
|
+
STDOUT.puts result
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Solargraph
|
4
|
+
module Snippets
|
5
|
+
def self.definitions
|
6
|
+
@definitions ||= JSON.parse('{
|
7
|
+
"Exception block": {
|
8
|
+
"prefix": "begin",
|
9
|
+
"body": [
|
10
|
+
"begin",
|
11
|
+
"\t$1",
|
12
|
+
"rescue => exception",
|
13
|
+
"\t",
|
14
|
+
"end"
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"Exception block with ensure": {
|
18
|
+
"prefix": "begin ensure",
|
19
|
+
"body": [
|
20
|
+
"begin",
|
21
|
+
"\t$1",
|
22
|
+
"rescue => exception",
|
23
|
+
"\t",
|
24
|
+
"ensure",
|
25
|
+
"\t",
|
26
|
+
"end"
|
27
|
+
]
|
28
|
+
},
|
29
|
+
"Exception block with else": {
|
30
|
+
"prefix": "begin else",
|
31
|
+
"body": [
|
32
|
+
"begin",
|
33
|
+
"\t$1",
|
34
|
+
"rescue => exception",
|
35
|
+
"\t",
|
36
|
+
"else",
|
37
|
+
"\t",
|
38
|
+
"end"
|
39
|
+
]
|
40
|
+
},
|
41
|
+
"Exception block with else and ensure": {
|
42
|
+
"prefix": "begin else ensure",
|
43
|
+
"body": [
|
44
|
+
"begin",
|
45
|
+
"\t$1",
|
46
|
+
"rescue => exception",
|
47
|
+
"\t",
|
48
|
+
"else",
|
49
|
+
"\t",
|
50
|
+
"ensure",
|
51
|
+
"\t",
|
52
|
+
"end"
|
53
|
+
]
|
54
|
+
},
|
55
|
+
"Class definition with initialize": {
|
56
|
+
"prefix": "class init",
|
57
|
+
"body": [
|
58
|
+
"class ${ClassName}",
|
59
|
+
"\tdef initialize",
|
60
|
+
"\t\t$0",
|
61
|
+
"\tend",
|
62
|
+
"end"
|
63
|
+
]
|
64
|
+
},
|
65
|
+
"Class definition": {
|
66
|
+
"prefix": "class",
|
67
|
+
"body": [
|
68
|
+
"class ${ClassName}",
|
69
|
+
"\t$0",
|
70
|
+
"end"
|
71
|
+
]
|
72
|
+
},
|
73
|
+
"for loop": {
|
74
|
+
"prefix": "for",
|
75
|
+
"body": [
|
76
|
+
"for ${value} in ${enumerable} do",
|
77
|
+
"\t$0",
|
78
|
+
"end"
|
79
|
+
]
|
80
|
+
},
|
81
|
+
"if": {
|
82
|
+
"prefix": "if",
|
83
|
+
"body": [
|
84
|
+
"if ${test}",
|
85
|
+
"\t$0",
|
86
|
+
"end"
|
87
|
+
]
|
88
|
+
},
|
89
|
+
"if else": {
|
90
|
+
"prefix": "if else",
|
91
|
+
"body": [
|
92
|
+
"if ${test}",
|
93
|
+
"\t$0",
|
94
|
+
"else",
|
95
|
+
"\t",
|
96
|
+
"end"
|
97
|
+
]
|
98
|
+
},
|
99
|
+
"if elsif": {
|
100
|
+
"prefix": "if elsif",
|
101
|
+
"body": [
|
102
|
+
"if ${test}",
|
103
|
+
"\t$0",
|
104
|
+
"elsif ",
|
105
|
+
"\t",
|
106
|
+
"end"
|
107
|
+
]
|
108
|
+
},
|
109
|
+
"if elsif else": {
|
110
|
+
"prefix": "if elsif else",
|
111
|
+
"body": [
|
112
|
+
"if ${test}",
|
113
|
+
"\t$0",
|
114
|
+
"elsif ",
|
115
|
+
"\t",
|
116
|
+
"else",
|
117
|
+
"\t",
|
118
|
+
"end"
|
119
|
+
]
|
120
|
+
},
|
121
|
+
"forever loop": {
|
122
|
+
"prefix": "loop",
|
123
|
+
"body": [
|
124
|
+
"loop do",
|
125
|
+
"\t$0",
|
126
|
+
"end"
|
127
|
+
]
|
128
|
+
},
|
129
|
+
"Module definition": {
|
130
|
+
"prefix": "module",
|
131
|
+
"body": [
|
132
|
+
"module ${ModuleName}",
|
133
|
+
"\t$0",
|
134
|
+
"end"
|
135
|
+
]
|
136
|
+
},
|
137
|
+
"unless": {
|
138
|
+
"prefix": "unless",
|
139
|
+
"body": [
|
140
|
+
"unless ${test}",
|
141
|
+
"\t$0",
|
142
|
+
"end"
|
143
|
+
]
|
144
|
+
},
|
145
|
+
"until loop": {
|
146
|
+
"prefix": "until",
|
147
|
+
"body": [
|
148
|
+
"until ${test}",
|
149
|
+
"\t$0",
|
150
|
+
"end"
|
151
|
+
]
|
152
|
+
},
|
153
|
+
"while loop": {
|
154
|
+
"prefix": "while",
|
155
|
+
"body": [
|
156
|
+
"while ${test}",
|
157
|
+
"\t$0",
|
158
|
+
"end"
|
159
|
+
]
|
160
|
+
},
|
161
|
+
"method definition": {
|
162
|
+
"prefix": "def",
|
163
|
+
"body": [
|
164
|
+
"def ${method_name}",
|
165
|
+
"\t$0",
|
166
|
+
"end"
|
167
|
+
],
|
168
|
+
"description": "Snippet for method definitions"
|
169
|
+
},
|
170
|
+
"defined?": {
|
171
|
+
"prefix": "defined?",
|
172
|
+
"body": [
|
173
|
+
"defined?"
|
174
|
+
],
|
175
|
+
"description": "Something silly"
|
176
|
+
}
|
177
|
+
}')
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.keywords
|
181
|
+
@keywords ||= [
|
182
|
+
'begin', 'class', 'for', 'if', 'module', 'unless', 'until', 'while', 'def', 'defined?'
|
183
|
+
]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Solargraph
|
4
|
+
|
5
|
+
class Suggestion
|
6
|
+
CLASS = 'Class'
|
7
|
+
KEYWORD = 'Keyword'
|
8
|
+
MODULE = 'Module'
|
9
|
+
METHOD = 'Method'
|
10
|
+
VARIABLE = 'Variable'
|
11
|
+
SNIPPET = 'Snippet'
|
12
|
+
|
13
|
+
attr_reader :label, :kind, :insert, :detail, :documentation
|
14
|
+
|
15
|
+
def initialize label, kind: KEYWORD, insert: nil, detail: nil, documentation: nil, code_object: nil
|
16
|
+
@label = label.to_s
|
17
|
+
@kind = kind
|
18
|
+
@insert = insert || @label
|
19
|
+
@detail = detail
|
20
|
+
@code_object = code_object
|
21
|
+
@documentation = documentation
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
label
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_json args={}
|
29
|
+
{
|
30
|
+
label: @label,
|
31
|
+
kind: @kind,
|
32
|
+
insert: @insert,
|
33
|
+
detail: @detail,
|
34
|
+
#documentation: (@documentation.nil? ? nil : @documentation.all)
|
35
|
+
documentation: (@code_object.nil? ? nil : YARD::Templates::Engine.render(format: :text, object: @code_object))
|
36
|
+
}.to_json(args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'parser/current'
|
3
|
+
require 'yard'
|
4
|
+
|
5
|
+
module Solargraph
|
6
|
+
|
7
|
+
class YardMap
|
8
|
+
def initialize required: [], workspace: nil
|
9
|
+
unless workspace.nil?
|
10
|
+
wsy = File.join(workspace, '.yardoc')
|
11
|
+
yardocs.push wsy if File.exist?(wsy)
|
12
|
+
end
|
13
|
+
required.each { |r|
|
14
|
+
gy = YARD::Registry.yardoc_file_for_gem(r)
|
15
|
+
yardocs.push gy unless gy.nil?
|
16
|
+
}
|
17
|
+
yardocs.push File.join(Dir.home, '.solargraph', 'cache', '2.0.0', 'yardoc')
|
18
|
+
#yardocs.push File.join(Dir.home, '.solargraph', 'cache', '2.0.0', 'yardoc-stdlib')
|
19
|
+
end
|
20
|
+
|
21
|
+
def yardocs
|
22
|
+
@yardocs ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_constants namespace, scope = ''
|
26
|
+
consts = []
|
27
|
+
result = []
|
28
|
+
yardocs.each { |y|
|
29
|
+
yard = YARD::Registry.load! y
|
30
|
+
unless yard.nil?
|
31
|
+
ns = nil
|
32
|
+
if scope == ''
|
33
|
+
ns = yard.at(namespace)
|
34
|
+
else
|
35
|
+
ns = yard.resolve(P(scope), namespace)
|
36
|
+
end
|
37
|
+
consts += ns.children unless ns.nil?
|
38
|
+
end
|
39
|
+
}
|
40
|
+
consts.each { |c|
|
41
|
+
result.push Suggestion.new(c.to_s.split('::').last, kind: Suggestion::CLASS)
|
42
|
+
}
|
43
|
+
result
|
44
|
+
end
|
45
|
+
|
46
|
+
def at signature
|
47
|
+
yardocs.each { |y|
|
48
|
+
yard = YARD::Registry.load! y
|
49
|
+
unless yard.nil?
|
50
|
+
obj = yard.at(signature)
|
51
|
+
return obj unless obj.nil?
|
52
|
+
end
|
53
|
+
}
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def resolve signature, scope
|
58
|
+
yardocs.each { |y|
|
59
|
+
yard = YARD::Registry.load! y
|
60
|
+
unless yard.nil?
|
61
|
+
obj = yard.resolve(P(scope), signature)
|
62
|
+
return obj unless obj.nil?
|
63
|
+
end
|
64
|
+
}
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_methods namespace, scope = ''
|
69
|
+
meths = []
|
70
|
+
yardocs.each { |y|
|
71
|
+
yard = YARD::Registry.load! y
|
72
|
+
unless yard.nil?
|
73
|
+
ns = nil
|
74
|
+
if scope == ''
|
75
|
+
ns = yard.at(namespace)
|
76
|
+
else
|
77
|
+
ns = yard.resolve(P(scope), namespace)
|
78
|
+
end
|
79
|
+
unless ns.nil?
|
80
|
+
ns.meths(scope: :class, visibility: [:public]).each { |m|
|
81
|
+
n = m.to_s.split('.').last
|
82
|
+
meths.push Suggestion.new("#{n}", kind: Suggestion::METHOD) if n.to_s.match(/^[a-z]/i)
|
83
|
+
}
|
84
|
+
if ns.kind_of?(YARD::CodeObjects::ClassObject) and namespace != 'Class'
|
85
|
+
meths += get_instance_methods('Class')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
}
|
90
|
+
meths
|
91
|
+
end
|
92
|
+
|
93
|
+
def get_instance_methods namespace, scope = ''
|
94
|
+
meths = []
|
95
|
+
yardocs.each { |y|
|
96
|
+
yard = YARD::Registry.load! y
|
97
|
+
unless yard.nil?
|
98
|
+
ns = nil
|
99
|
+
if scope == ''
|
100
|
+
ns = yard.at(namespace)
|
101
|
+
else
|
102
|
+
ns = yard.resolve(P(scope), namespace)
|
103
|
+
end
|
104
|
+
unless ns.nil?
|
105
|
+
ns.meths(scope: :instance, visibility: [:public]).each { |m|
|
106
|
+
n = m.to_s.split('#').last
|
107
|
+
meths.push Suggestion.new("#{n}", kind: Suggestion::METHOD, documentation: m.docstring, code_object: m) if n.to_s.match(/^[a-z]/i) and !m.to_s.start_with?('Kernel#') and !m.docstring.to_s.include?(':nodoc:')
|
108
|
+
}
|
109
|
+
if ns.kind_of?(YARD::CodeObjects::ClassObject) and namespace != 'Object'
|
110
|
+
meths += get_instance_methods('Object')
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
}
|
115
|
+
meths
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|