yoda-language-server 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +5 -3
  3. data/README.md +50 -48
  4. data/client/atom/main.js +13 -3
  5. data/client/vscode/.vscode/launch.json +7 -4
  6. data/client/vscode/package-lock.json +585 -1454
  7. data/client/vscode/package.json +10 -7
  8. data/client/vscode/src/extension.ts +3 -3
  9. data/client/vscode/src/test/completion.test.ts +39 -0
  10. data/client/vscode/src/test/helper.ts +38 -0
  11. data/client/vscode/src/test/index.ts +5 -3
  12. data/client/vscode/testFixture/completion.rb +1 -0
  13. data/exe/yoda +1 -20
  14. data/lib/yoda.rb +2 -1
  15. data/lib/yoda/commands.rb +34 -0
  16. data/lib/yoda/commands/base.rb +10 -0
  17. data/lib/yoda/commands/complete.rb +36 -0
  18. data/lib/yoda/commands/file_cursor_parsable.rb +29 -0
  19. data/lib/yoda/{runner → commands}/infer.rb +4 -9
  20. data/lib/yoda/commands/setup.rb +37 -0
  21. data/lib/yoda/errors.rb +34 -0
  22. data/lib/yoda/evaluation/evaluator.rb +2 -0
  23. data/lib/yoda/server.rb +60 -15
  24. data/lib/yoda/server/completion_provider.rb +8 -8
  25. data/lib/yoda/server/definition_provider.rb +8 -8
  26. data/lib/yoda/server/hover_provider.rb +6 -6
  27. data/lib/yoda/server/initialization_provider.rb +85 -0
  28. data/lib/yoda/server/{client_info.rb → session.rb} +6 -2
  29. data/lib/yoda/server/signature_provider.rb +6 -6
  30. data/lib/yoda/store/actions.rb +3 -1
  31. data/lib/yoda/store/actions/build_core_index.rb +44 -0
  32. data/lib/yoda/store/actions/import_core_library.rb +14 -9
  33. data/lib/yoda/store/actions/import_gem.rb +98 -0
  34. data/lib/yoda/store/actions/import_std_library.rb +35 -0
  35. data/lib/yoda/store/adapters.rb +1 -0
  36. data/lib/yoda/store/adapters/memory_adapter.rb +81 -0
  37. data/lib/yoda/store/objects.rb +4 -0
  38. data/lib/yoda/store/objects/addressable.rb +0 -12
  39. data/lib/yoda/store/objects/base.rb +4 -14
  40. data/lib/yoda/store/objects/merger.rb +4 -4
  41. data/lib/yoda/store/objects/patchable.rb +19 -0
  42. data/lib/yoda/store/objects/project_status.rb +169 -0
  43. data/lib/yoda/store/objects/serializable.rb +39 -0
  44. data/lib/yoda/store/project.rb +28 -114
  45. data/lib/yoda/store/project/cache.rb +79 -0
  46. data/lib/yoda/store/project/library_doc_loader.rb +102 -0
  47. data/lib/yoda/store/query/find_constant.rb +2 -1
  48. data/lib/yoda/store/registry.rb +15 -0
  49. data/lib/yoda/store/yard_importer.rb +58 -28
  50. data/lib/yoda/typing/evaluator.rb +8 -5
  51. data/lib/yoda/version.rb +1 -1
  52. data/package.json +32 -11
  53. data/scripts/benchmark.rb +1 -1
  54. data/yoda-language-server.gemspec +1 -0
  55. metadata +37 -7
  56. data/client/vscode/src/test/extension.test.ts +0 -22
  57. data/lib/yoda/runner/setup.rb +0 -26
  58. data/lib/yoda/store/actions/import_gems.rb +0 -91
@@ -3,9 +3,9 @@
3
3
  "displayName": "yoda",
4
4
  "description": "Static analytics tool for Ruby",
5
5
  "version": "0.0.1",
6
- "publisher": "nemunemu",
6
+ "publisher": "tomoasleep",
7
7
  "engines": {
8
- "vscode": "^1.19.0"
8
+ "vscode": "^1.23.0"
9
9
  },
10
10
  "categories": [
11
11
  "Other"
@@ -27,13 +27,16 @@
27
27
  "compile": "tsc -p ./",
28
28
  "watch": "tsc -watch -p ./",
29
29
  "postinstall": "node ./node_modules/vscode/bin/install",
30
+ "update-vscode": "node ./node_modules/vscode/bin/install",
30
31
  "test": "npm run compile && node ./node_modules/vscode/bin/test"
31
32
  },
33
+ "dependencies": {
34
+ "vscode": "^1.1.18",
35
+ "vscode-languageclient": "^4.1.4"
36
+ },
32
37
  "devDependencies": {
33
- "typescript": "^2.6.1",
34
- "vscode": "^1.1.6",
35
- "vscode-languageclient": "^3.5.0",
36
- "@types/node": "^7.0.43",
37
- "@types/mocha": "^2.2.42"
38
+ "@types/mocha": "^5.2.0",
39
+ "@types/node": "^8.0.0",
40
+ "typescript": "^2.9.2"
38
41
  }
39
42
  }
@@ -17,7 +17,7 @@ export function activate(context: vscode.ExtensionContext) {
17
17
  console.log('Congratulations, your extension "yoda" is now active!');
18
18
 
19
19
  let execOptions = {
20
- command: path.resolve(__dirname, '../../../exe/yoda'),
20
+ command: 'yoda',
21
21
  args: ['server'],
22
22
  }
23
23
 
@@ -34,9 +34,9 @@ export function activate(context: vscode.ExtensionContext) {
34
34
  }
35
35
  }
36
36
 
37
- let disposable = new LanguageClient('yoda', 'Yoda Language Server', serverOptions, clientOptions).start();
37
+ let disposable = new LanguageClient('yoda', 'Yoda', serverOptions, clientOptions).start();
38
38
  }
39
39
 
40
40
  // this method is called when your extension is deactivated
41
41
  export function deactivate() {
42
- }
42
+ }
@@ -0,0 +1,39 @@
1
+ import * as vscode from 'vscode';
2
+ import * as assert from 'assert';
3
+ import { getDocUri, activate } from './helper';
4
+
5
+ describe('Should do completion', () => {
6
+ const docUri = getDocUri('completion.rb');
7
+
8
+ it('Completes', async () => {
9
+ await testCompletion(docUri, new vscode.Position(0, 2), {
10
+ items: [
11
+ { label: 'Object', kind: vscode.CompletionItemKind.Class },
12
+ ]
13
+ });
14
+ })
15
+
16
+
17
+ });
18
+
19
+ async function testCompletion(
20
+ docUri: vscode.Uri,
21
+ position: vscode.Position,
22
+ expectedCompletionList: vscode.CompletionList
23
+ ) {
24
+ await activate(docUri);
25
+
26
+ // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion
27
+ const actualCompletionList = (await vscode.commands.executeCommand(
28
+ 'vscode.executeCompletionItemProvider',
29
+ docUri,
30
+ position
31
+ )) as vscode.CompletionList;
32
+
33
+ assert.equal(actualCompletionList.items.length, expectedCompletionList.items.length);
34
+ expectedCompletionList.items.forEach((expectedItem, i) => {
35
+ const actualItem = actualCompletionList.items[i];
36
+ assert.equal(actualItem.label, expectedItem.label);
37
+ assert.equal(actualItem.kind, expectedItem.kind);
38
+ });
39
+ }
@@ -0,0 +1,38 @@
1
+ import * as vscode from 'vscode';
2
+ import * as path from 'path';
3
+
4
+ export let doc: vscode.TextDocument;
5
+ export let editor: vscode.TextEditor;
6
+ export let documentEol: string;
7
+ export let platformEol: string;
8
+
9
+ export async function activate(docUri: vscode.Uri) {
10
+ const ext = vscode.extensions.getExtension('tomoasleep.yoda');
11
+ await ext.activate();
12
+ try {
13
+ doc = await vscode.workspace.openTextDocument(docUri);
14
+ editor = await vscode.window.showTextDocument(doc);
15
+ await sleep(20000); // Wait for server activation
16
+ } catch (e) {
17
+ console.error(e);
18
+ }
19
+ }
20
+
21
+ async function sleep(ms: number) {
22
+ return new Promise(resolve => setTimeout(resolve, ms));
23
+ }
24
+
25
+ export const getDocPath = (p: string) => {
26
+ return path.resolve(__dirname, '../../testFixture', p);
27
+ };
28
+ export const getDocUri = (p: string) => {
29
+ return vscode.Uri.file(getDocPath(p));
30
+ };
31
+
32
+ export async function setTestContent(content: string): Promise<boolean> {
33
+ const all = new vscode.Range(
34
+ doc.positionAt(0),
35
+ doc.positionAt(doc.getText().length)
36
+ );
37
+ return editor.edit(eb => eb.replace(all, content));
38
+ }
@@ -15,8 +15,10 @@ import * as testRunner from 'vscode/lib/testrunner';
15
15
  // You can directly control Mocha options by uncommenting the following lines
16
16
  // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info
17
17
  testRunner.configure({
18
- ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
19
- useColors: true // colored output from test results
18
+ ui: 'bdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.)
19
+ useColors: true, // colored output from test results
20
+ timeout: 600000,
21
+
20
22
  });
21
23
 
22
- module.exports = testRunner;
24
+ module.exports = testRunner;
data/exe/yoda CHANGED
@@ -5,23 +5,4 @@ if Dir.exist?(File.join(__dir__, "..", ".git"))
5
5
  end
6
6
 
7
7
  require 'yoda'
8
- require 'optparse'
9
-
10
- opt = OptionParser.new("Usage: yoda [options]") do |opt|
11
- end
12
-
13
- argv = opt.parse(ARGV)
14
-
15
- case argv.first
16
- when 'setup'
17
- require 'pry'
18
- Pry::rescue { Yoda::Runner::Setup.run(argv[1]) }
19
- when 'infer'
20
- require 'pry'
21
- Pry::rescue { Yoda::Runner::Infer.run(argv[1]) }
22
- when 'server'
23
- Yoda::Server.new.run
24
- else
25
- puts opt.help
26
- exit 1
27
- end
8
+ Yoda::Commands::Top.start(ARGV)
@@ -1,11 +1,12 @@
1
1
  module Yoda
2
2
  require "yoda/version"
3
+ require "yoda/commands"
4
+ require "yoda/errors"
3
5
  require "yoda/evaluation"
4
6
  require "yoda/model"
5
7
  require "yoda/store"
6
8
  require "yoda/server"
7
9
  require "yoda/parsing"
8
10
  require "yoda/typing"
9
- require "yoda/runner"
10
11
  require "yoda/yard_extensions"
11
12
  end
@@ -0,0 +1,34 @@
1
+ require 'thor'
2
+
3
+ module Yoda
4
+ # Commands module has handler for each cli command.
5
+ module Commands
6
+ require 'yoda/commands/base'
7
+ require 'yoda/commands/file_cursor_parsable'
8
+ require 'yoda/commands/setup'
9
+ require 'yoda/commands/infer'
10
+ require 'yoda/commands/complete'
11
+
12
+ class Top < Thor
13
+ desc 'setup', 'Setup indexes for current Ruby version and project gems'
14
+ def setup
15
+ Commands::Setup.run
16
+ end
17
+
18
+ desc 'infer POSITION', 'Infer the type of value at the specified position'
19
+ def infer(position)
20
+ Commands::Infer.run(position)
21
+ end
22
+
23
+ desc 'complete POSITION', 'Provide completion candidates for the specified position'
24
+ def complete(position)
25
+ Commands::Complete.run(position)
26
+ end
27
+
28
+ desc 'server', 'Start Language Server'
29
+ def server
30
+ Server.new.run
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ module Yoda
2
+ module Commands
3
+ # @abstract
4
+ class Base
5
+ def self.run(*args)
6
+ self.new(*args).run
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,36 @@
1
+ module Yoda
2
+ module Commands
3
+ class Complete < Base
4
+ include FileCursorParsable
5
+
6
+ attr_reader :filename_with_position
7
+
8
+ # @param filename_with_position [String] position representation with the format `path/to/file:line_num:character_num`
9
+ def initialize(filename_with_position)
10
+ @filename_with_position = filename_with_position
11
+ end
12
+
13
+ def run
14
+ project.build_cache
15
+ puts create_signature_help(worker.current_node_signature)
16
+ end
17
+
18
+ private
19
+
20
+ # @param signature [Model::NodeSignature, nil]
21
+ # @return [String, nil]
22
+ def create_signature_help(signature)
23
+ return nil unless signature
24
+ signature.descriptions.map(&:title).join("\n")
25
+ end
26
+
27
+ def worker
28
+ @worker ||= Evaluation::CurrentNodeExplain.new(project.registry, File.read(filename), position)
29
+ end
30
+
31
+ def project
32
+ @project ||= Store::Project.new(Dir.pwd)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ module Yoda
2
+ module Commands
3
+ # Provide parsing methods of positon representation with the format `path/to/file:line_num:character_num`
4
+ module FileCursorParsable
5
+ private
6
+
7
+ # Returns a cursor literal to parse.
8
+ # @abstract
9
+ # @return [String]
10
+ def filename_with_position
11
+ fail NotImplementedError
12
+ end
13
+
14
+ # @return [String, nil] represents the filename part.
15
+ def filename
16
+ @filename ||= filename_with_position.split(':').first
17
+ end
18
+
19
+ # Parse location part of cursor literal and returns the parsed location.
20
+ # @return [Parsing::Location]
21
+ def position
22
+ @position ||= begin
23
+ row, column = filename_with_position.split(':').slice(1..2)
24
+ Parsing::Location.new(row: row.to_i, column: column.to_i)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,20 +1,15 @@
1
1
  module Yoda
2
- module Runner
3
- class Infer
2
+ module Commands
3
+ class Infer < Base
4
4
  attr_reader :filename_with_position
5
5
 
6
- # @param filename_with_position [String]
7
- def self.run(filename_with_position)
8
- new(filename_with_position).run
9
- end
10
-
11
- # @param filename_with_position [String]
6
+ # @param filename_with_position [String] position representation with the format `path/to/file:line_num:character_num`
12
7
  def initialize(filename_with_position)
13
8
  @filename_with_position = filename_with_position
14
9
  end
15
10
 
16
11
  def run
17
- project.setup
12
+ project.build_cache
18
13
  puts create_signature_help(worker.current_node_signature)
19
14
  end
20
15
 
@@ -0,0 +1,37 @@
1
+ module Yoda
2
+ module Commands
3
+ class Setup < Base
4
+ # @return [String]
5
+ attr_reader :dir
6
+
7
+ # @return [true, false]
8
+ attr_reader :force_build
9
+
10
+ # @param dir [String]
11
+ def initialize(dir: nil, force_build: false)
12
+ @dir = dir || Dir.pwd
13
+ @force_build = force_build
14
+ end
15
+
16
+ def run
17
+ build_core_index
18
+ if File.exist?(File.expand_path('Gemfile.lock', dir)) || force_build
19
+ STDERR.puts 'Building index for the current project...'
20
+ force_build ? project.rebuild_cache(progress: true) : project.build_cache(progress: true)
21
+ else
22
+ STDERR.puts 'Skipped building project index because Gemfile.lock is not exist for the current dir'
23
+ end
24
+ end
25
+
26
+ def project
27
+ @project ||= Store::Project.new(dir)
28
+ end
29
+
30
+ private
31
+
32
+ def build_core_index
33
+ Store::Actions::BuildCoreIndex.run unless Store::Actions::BuildCoreIndex.exists?
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ module Yoda
2
+ # @abstract
3
+ class BaseError < ::StandardError
4
+ end
5
+
6
+ class GemImportError < BaseError
7
+ # @return [String]
8
+ attr_reader :name, :version
9
+
10
+ def initialize(name:, version:)
11
+ @name = name
12
+ @version = version
13
+ super(msg)
14
+ end
15
+
16
+ def msg
17
+ "Failed to import #{name} #{version}"
18
+ end
19
+ end
20
+
21
+ class CoreImportError < BaseError
22
+ # @return [String]
23
+ attr_reader :name
24
+
25
+ def initialize(name)
26
+ @name = name
27
+ super(msg)
28
+ end
29
+
30
+ def msg
31
+ "Failed to import Ruby #{name} Library"
32
+ end
33
+ end
34
+ end
@@ -76,6 +76,7 @@ module Yoda
76
76
  def evaluation_context
77
77
  @evaluation_context ||= begin
78
78
  fail RuntimeError, "The namespace #{scope.scope_name} (#{scope}) is not registered" unless scope_constant
79
+ fail RuntimeError, "The namespace for #{scope} (#{scope.scope_name}) is not registered" unless receiver
79
80
  lexical_scope = Typing::LexicalScope.new(scope_constant, scope.ancestor_scopes)
80
81
  context = Typing::Context.new(registry: registry, caller_object: receiver, lexical_scope: lexical_scope)
81
82
  context.env.bind_method_parameters(current_method_signature) if current_method_signature
@@ -89,6 +90,7 @@ module Yoda
89
90
  @current_method_signature ||= Store::Query::FindSignature.new(registry).select(scope_constant, scope.name.to_s)&.first
90
91
  end
91
92
 
93
+ # @return [Store::Objects::Base, nil]
92
94
  def receiver
93
95
  @receiver ||= begin
94
96
  if scope.kind == :method
@@ -1,4 +1,5 @@
1
1
  require 'language_server-protocol'
2
+ require 'securerandom'
2
3
 
3
4
  module Yoda
4
5
  class Server
@@ -6,8 +7,9 @@ module Yoda
6
7
  require 'yoda/server/signature_provider'
7
8
  require 'yoda/server/hover_provider'
8
9
  require 'yoda/server/definition_provider'
10
+ require 'yoda/server/initialization_provider'
9
11
  require 'yoda/server/deserializer'
10
- require 'yoda/server/client_info'
12
+ require 'yoda/server/session'
11
13
 
12
14
  LSP = ::LanguageServer::Protocol
13
15
 
@@ -21,8 +23,8 @@ module Yoda
21
23
  # @type ::LanguageServer::Protocol::Transport::Stdio::Writer
22
24
  attr_reader :writer
23
25
 
24
- # @type ClientInfo
25
- attr_reader :client_info
26
+ # @return [Responser]
27
+ attr_reader :session
26
28
 
27
29
  # @type CompletionProvider
28
30
  attr_reader :completion_provider
@@ -36,17 +38,26 @@ module Yoda
36
38
  # @type DefinitionProvider
37
39
  attr_reader :definition_provider
38
40
 
41
+ # @return [Array<Hash>]
42
+ attr_reader :after_notifications
43
+
39
44
  def initialize
40
45
  @reader = LSP::Transport::Stdio::Reader.new
41
46
  @writer = LSP::Transport::Stdio::Writer.new
47
+ @after_notifications = []
42
48
  end
43
49
 
44
50
  def run
45
51
  reader.read do |request|
46
52
  begin
47
53
  if result = callback(request)
48
- writer.write(id: request[:id], result: result)
54
+ if result.is_a?(LanguageServer::Protocol::Interface::ResponseError)
55
+ send_error(id: request[:id], error: result)
56
+ else
57
+ send_response(id: request[:id], result: result)
58
+ end
49
59
  end
60
+ process_after_notifications if session&.client_initialized
50
61
  rescue StandardError => ex
51
62
  STDERR.puts ex
52
63
  STDERR.puts ex.backtrace
@@ -54,6 +65,30 @@ module Yoda
54
65
  end
55
66
  end
56
67
 
68
+ def process_after_notifications
69
+ while notification = after_notifications.pop
70
+ send_notification(**notification)
71
+ end
72
+ end
73
+
74
+ # @param method [String]
75
+ # @param params [Object]
76
+ def send_notification(method:, params:)
77
+ writer.write(method: method, params: params)
78
+ end
79
+
80
+ # @param id [String]
81
+ # @param result [Object]
82
+ def send_response(id:, result:)
83
+ writer.write(id: id, result: result)
84
+ end
85
+
86
+ # @param method [String]
87
+ # @param error [Object]
88
+ def send_error(id:, error:)
89
+ writer.write(id: id, error: error)
90
+ end
91
+
57
92
  def callback(request)
58
93
  if method_name = resolve(request_registrations, request[:method])
59
94
  send(method_name, self.class.deserialize(request[:params] || {}))
@@ -65,10 +100,12 @@ module Yoda
65
100
 
66
101
  # @param hash [Hash]
67
102
  # @param key [String, Symbol]
103
+ # @return [Symbol, nil]
68
104
  def resolve(hash, key)
69
- key.to_s.split('/').reduce(hash) do |scope, key|
105
+ resolved = key.to_s.split('/').reduce(hash) do |scope, key|
70
106
  (scope || {})[key.to_sym]
71
107
  end
108
+ resolved.is_a?(Symbol) && resolved
72
109
  end
73
110
 
74
111
  def request_registrations
@@ -97,12 +134,13 @@ module Yoda
97
134
  end
98
135
 
99
136
  def handle_initialize(params)
100
- @client_info = ClientInfo.new(params[:root_uri])
101
- @completion_provider = CompletionProvider.new(@client_info)
102
- @hover_provider = HoverProvider.new(@client_info)
103
- @signature_provider = SignatureProvider.new(@client_info)
104
- @definition_provider = DefinitionProvider.new(@client_info)
105
- client_info.setup
137
+ @session = Session.new(params[:root_uri])
138
+ @completion_provider = CompletionProvider.new(@session)
139
+ @hover_provider = HoverProvider.new(@session)
140
+ @signature_provider = SignatureProvider.new(@session)
141
+ @definition_provider = DefinitionProvider.new(@session)
142
+
143
+ (InitializationProvider.new(@session).provide || []).each { |notification| after_notifications.push(notification) }
106
144
 
107
145
  LSP::Interface::InitializeResult.new(
108
146
  capabilities: LSP::Interface::ServerCapabilities.new(
@@ -113,7 +151,7 @@ module Yoda
113
151
  ),
114
152
  ),
115
153
  completion_provider: LSP::Interface::CompletionOptions.new(
116
- resolve_provider: true,
154
+ resolve_provider: false,
117
155
  trigger_characters: ['.', '@', '[', ':', '!', '<'],
118
156
  ),
119
157
  hover_provider: true,
@@ -123,9 +161,16 @@ module Yoda
123
161
  ),
124
162
  ),
125
163
  )
164
+ rescue => e
165
+ LanguageServer::Protocol::Interface::ResponseError.new(
166
+ message: "Failed to initialize yoda: #{e.class} #{e.message}",
167
+ code: LanguageServer::Protocol::Constant::InitializeError::UNKNOWN_PROTOCOL_VERSION,
168
+ data: LanguageServer::Protocol::Interface::InitializeError.new(retry: false),
169
+ )
126
170
  end
127
171
 
128
172
  def handle_initialized(_params)
173
+ session.client_initialized = true
129
174
  end
130
175
 
131
176
  def handle_shutdown(_params)
@@ -143,19 +188,19 @@ module Yoda
143
188
  def handle_text_document_did_open(params)
144
189
  uri = params[:text_document][:uri]
145
190
  text = params[:text_document][:text]
146
- client_info.file_store.store(uri, text)
191
+ session.file_store.store(uri, text)
147
192
  end
148
193
 
149
194
  def handle_text_document_did_save(params)
150
195
  uri = params[:text_document][:uri]
151
196
 
152
- client_info.reparse_doc(uri)
197
+ session.reparse_doc(uri)
153
198
  end
154
199
 
155
200
  def handle_text_document_did_change(params)
156
201
  uri = params[:text_document][:uri]
157
202
  text = params[:content_changes].first[:text]
158
- client_info.file_store.store(uri, text)
203
+ session.file_store.store(uri, text)
159
204
  end
160
205
 
161
206
  def handle_text_document_completion(params)