Skip to content

Commit 91a3677

Browse files
committed
Jump to definition
Not perfect though...
1 parent 76a9c8b commit 91a3677

File tree

9 files changed

+258
-7
lines changed

9 files changed

+258
-7
lines changed

bin/console

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ require "language_server"
66
# You can add fixtures and/or initialization code here to make experimenting
77
# with your gem easier. You can also use a different console, if you like.
88

9+
def project
10+
@project ||= LanguageServer::Project.new(
11+
LanguageServer::FileStore.new(load_paths: $LOAD_PATH, remote_root: ENV['LANGUAGE_SERVER_RUBY_REMOTE_ROOT'], local_root: Dir.getwd)
12+
)
13+
end
14+
915
# (If you use this, don't forget to add pry to your Gemfile!)
1016
require "pry-byebug"
1117
Pry.start

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ services:
44
build: &app-build
55
context: .
66
dockerfile: Dockerfile.development
7+
environment:
8+
LANGUAGE_SERVER_RUBY_REMOTE_ROOT: $PWD
79
volumes:
810
- mtsmfm-language-server-sync:/app:nocopy
911
- home:/home/ruby

lib/language_server.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
require "language_server/linter/ruby_wc"
77
require "language_server/completion_provider/rcodetools"
88
require "language_server/completion_provider/ad_hoc"
9+
require "language_server/definition_provider/ad_hoc"
910
require "language_server/file_store"
1011
require "language_server/project"
1112

@@ -65,7 +66,8 @@ def on(method, &callback)
6566
completion_provider: Protocol::Interfaces::CompletionOptions.new(
6667
resolve_provider: true,
6768
trigger_characters: %w(.)
68-
)
69+
),
70+
definition_provider: true
6971
)
7072
)
7173
end
@@ -115,4 +117,13 @@ def on(method, &callback)
115117
CompletionProvider::Rcodetools.new(uri: uri, line: line, character: character, file_store: file_store)
116118
].flat_map(&:call)
117119
end
120+
121+
on :"textDocument/definition" do |request:, project:|
122+
uri = request[:params][:textDocument][:uri]
123+
line, character = request[:params][:position].fetch_values(:line, :character).map(&:to_i)
124+
125+
[
126+
DefinitionProvider::AdHoc.new(uri: uri, line: line, character: character, project: project),
127+
].flat_map(&:call)
128+
end
118129
end
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
module LanguageServer
2+
module DefinitionProvider
3+
class AdHoc
4+
def initialize(uri:, line:, character:, project:)
5+
@uri = uri
6+
@line = line
7+
@character = character
8+
@project = project
9+
end
10+
11+
def call
12+
project.find_definitions(uri: uri, line: line, character: character).map do |n|
13+
Protocol::Interfaces::Location.new(
14+
uri: "file://#{n.remote_path}",
15+
range: Protocol::Interfaces::Range.new(
16+
start: Protocol::Interfaces::Position.new(
17+
line: n.lines.begin,
18+
character: 0
19+
),
20+
end: Protocol::Interfaces::Position.new(
21+
line: n.lines.end,
22+
character: 0
23+
)
24+
)
25+
)
26+
end
27+
end
28+
29+
private
30+
31+
attr_reader :uri, :line, :character, :project
32+
end
33+
end
34+
end

lib/language_server/project.rb

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,19 @@ def initialize(file_store)
99
fetch_result
1010
end
1111

12+
def find_definitions(uri:, line:, character:)
13+
result = result_store[file_store.path_from_remote_uri(uri)]
14+
15+
ref = result.refs.select {|node| node.lines.include?(line) && node.characters.include?(character) }.min_by {|node| node.characters.size }
16+
17+
return [] unless ref
18+
19+
lazy_modules.select {|n| n.full_name == ref.full_name }.force + lazy_classes.select {|n| n.full_name == ref.full_name }.force
20+
end
21+
1222
def recalculate_result(uri)
1323
path = file_store.path_from_remote_uri(uri)
14-
result_store[path] = calculate(file_store.read(path), path)
24+
calculate(file_store.read(path), path)
1525
end
1626

1727
def constants(uri: nil, line: nil, character: nil)
@@ -50,7 +60,7 @@ def lazy_classes
5060

5161
def fetch_result
5262
file_store.each {|content, path|
53-
result_store[path] = calculate(content, path)
63+
calculate(content, path)
5464
}
5565
end
5666

@@ -61,7 +71,14 @@ def find_nearest_node(uri:, line:, character:)
6171
end
6272

6373
def calculate(content, path)
64-
Parser.parse(content, path)
74+
begin
75+
result = Parser.parse(content, path)
76+
rescue => e
77+
LanguageServer.logger.warn("Parse failed (local: #{path.local_path}, remote: #{path.remote_path})")
78+
LanguageServer.logger.warn(e)
79+
end
80+
81+
result_store[path] = result if result
6582
end
6683
end
6784
end

lib/language_server/project/node.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,69 @@ def inspect
9292
"<Class #{full_name}#L#{lines.begin}-#{lines.end}>"
9393
end
9494
end
95+
96+
class VarRef < Node
97+
attributes :node
98+
99+
def lines
100+
node.lineno..node.lineno
101+
end
102+
103+
def characters
104+
node.character..(character - 1)
105+
end
106+
107+
def unshift_namespace(class_or_module)
108+
node.unshift_namespace(class_or_module) if node.respond_to?(:unshift_namespace)
109+
end
110+
111+
def names
112+
node.names
113+
end
114+
115+
def name
116+
node.name
117+
end
118+
119+
def full_name
120+
names.join('::')
121+
end
122+
123+
def inspect
124+
"<VarRef #{full_name}#L#{lineno}(#{characters})>"
125+
end
126+
end
127+
128+
class ConstPathRef < Node
129+
attributes :nodes
130+
131+
def lines
132+
(nodes.first.lineno)..(nodes.last.lineno)
133+
end
134+
135+
def characters
136+
(nodes.first.characters.begin)..(nodes.last.character)
137+
end
138+
139+
def unshift_namespace(class_or_module)
140+
nodes.first.unshift_namespace(class_or_module)
141+
end
142+
143+
def name
144+
nodes.last.name
145+
end
146+
147+
def names
148+
nodes.flat_map(&:names)
149+
end
150+
151+
def full_name
152+
names.join('::')
153+
end
154+
155+
def inspect
156+
"<ConstPathRef #{full_name}#L#{lineno}(#{characters})>"
157+
end
158+
end
95159
end
96160
end

lib/language_server/project/parser.rb

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ module LanguageServer
55
class Project
66
class Parser < Ripper
77
class Result
8-
attr_reader :constants, :classes, :modules
8+
attr_reader :constants, :classes, :modules, :refs
99

1010
def initialize
1111
@constants = []
1212
@classes = []
1313
@modules = []
14+
@refs = []
1415
end
1516
end
1617

@@ -40,10 +41,34 @@ def lineno
4041
super - 1
4142
end
4243

44+
def on_var_ref(node)
45+
if node.instance_of?(Constant)
46+
build_node(VarRef, node: node).tap do |n|
47+
result.refs << n
48+
end
49+
else
50+
node
51+
end
52+
end
53+
54+
def on_const_path_ref(*nodes)
55+
if nodes.all? {|n| [Constant, ConstPathRef, VarRef].include?(n.class) }
56+
build_node(ConstPathRef, nodes: nodes).tap do |n|
57+
result.refs << n
58+
end
59+
else
60+
nodes
61+
end
62+
end
63+
4364
def on_const(name)
4465
build_node(Constant, namespaces: [], name: name, value: nil)
4566
end
4667

68+
def on_def(*args)
69+
args.flatten.compact
70+
end
71+
4772
def on_int(value)
4873
build_node(LiteralValue, value: value.to_i)
4974
end
@@ -60,15 +85,17 @@ def on_assign(left, right)
6085
end
6186

6287
def on_module(constant, children)
63-
cn = children.select {|child| child.instance_of?(Constant) || child.instance_of?(Module) || child.instance_of?(Class)}
88+
cn = children.select {|child| child.respond_to?(:unshift_namespace) }
89+
6490
build_node(Module, constant: constant, children: cn).tap do |m|
6591
result.modules << m
6692
cn.each {|child| child.unshift_namespace(m) }
6793
end
6894
end
6995

7096
def on_class(constant, superclass, children)
71-
cn = children.select {|child| child.instance_of?(Constant) || child.instance_of?(Module) || child.instance_of?(Class)}
97+
cn = children.select {|child| child.respond_to?(:unshift_namespace) }
98+
7299
build_node(Class, constant: constant, superclass: superclass, children: cn).tap do |c|
73100
result.classes << c
74101
cn.each {|child| child.unshift_namespace(c) }

test/language_server/project/parser_test.rb

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,69 @@ def initialize
6262

6363
assert { result.modules.map(&:full_name) == %w(Hi) }
6464
end
65+
66+
def test_ref
67+
result = Parser.parse(<<-EOS.strip_heredoc)
68+
A
69+
EOS
70+
71+
assert { result.refs.map {|r| [r.full_name, r.characters] } == [['A', 0..1]] }
72+
end
73+
74+
def test_ref_2
75+
result = Parser.parse(<<-EOS.strip_heredoc)
76+
A::B
77+
EOS
78+
79+
assert { result.refs.map {|r| [r.full_name, r.characters] } == [['A', 0..2], ['A::B', 0..3]] }
80+
end
81+
82+
def test_ref_3
83+
result = Parser.parse(<<-EOS.strip_heredoc)
84+
A::B::C
85+
EOS
86+
87+
assert { result.refs.map {|r| [r.full_name, r.characters] } == [['A', 0..2], ['A::B', 0..3], ['A::B::C', 0..6]] }
88+
end
89+
90+
def test_ref_within_module
91+
result = Parser.parse(<<-EOS.strip_heredoc)
92+
module A
93+
B
94+
end
95+
EOS
96+
97+
assert { result.refs.map {|r| [r.full_name, r.characters] } == [['A::B', 2..3]] }
98+
99+
result = Parser.parse(<<-EOS.strip_heredoc)
100+
module A
101+
B::C
102+
end
103+
EOS
104+
105+
assert { result.refs.map {|r| [r.full_name, r.characters] } == [['A::B', 2..4], ['A::B::C', 2..5]] }
106+
end
107+
108+
def test_const_ref_with_method
109+
result = Parser.parse(<<-EOS.strip_heredoc)
110+
class.self::FOO
111+
EOS
112+
113+
assert { result.refs == [] }
114+
end
115+
116+
def test_inline_nested_class
117+
result = Parser.parse(<<-EOS.strip_heredoc)
118+
module A
119+
module B
120+
class C::D::E
121+
end
122+
end
123+
end
124+
EOS
125+
126+
assert { result.classes.map(&:full_name) == %w(A::B::C::D::E) }
127+
end
65128
end
66129
end
67130
end

test/language_server/project_test.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,32 @@ def hi
4646
assert { project.classes(uri: uri, line: 1, character: 0).map(&:full_name).uniq.sort == %w(A B).sort }
4747
assert { project.classes(uri: uri, line: 2, character: 0).map(&:full_name).uniq.sort == %w(A B A::A::C).sort }
4848
end
49+
50+
def test_find_definition
51+
store = FileStore.new
52+
store.cache('file:///a.rb', <<-EOS.strip_heredoc)
53+
class A
54+
module A
55+
class C
56+
def foo
57+
end
58+
end
59+
end
60+
end
61+
EOS
62+
63+
store.cache('file:///b.rb', <<-EOS.strip_heredoc)
64+
class A
65+
def hi
66+
A::C.foo
67+
end
68+
end
69+
EOS
70+
71+
project = Project.new(store)
72+
73+
assert { project.find_definitions(uri: 'file:///b.rb', line: 2, character: 6).first.full_name == 'A::A' }
74+
assert { project.find_definitions(uri: 'file:///b.rb', line: 2, character: 7).first.full_name == 'A::A::C' }
75+
end
4976
end
5077
end

0 commit comments

Comments
 (0)