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