yoda-language-server 0.4.0 → 0.5.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.
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)