holistic-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +3 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +52 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +35 -0
  8. data/Rakefile +8 -0
  9. data/config/logging.rb +6 -0
  10. data/exe/holistic-ruby +6 -0
  11. data/holistic-ruby.gemspec +34 -0
  12. data/lib/holistic/application.rb +29 -0
  13. data/lib/holistic/background_process.rb +11 -0
  14. data/lib/holistic/database/table.rb +78 -0
  15. data/lib/holistic/document/cursor.rb +9 -0
  16. data/lib/holistic/document/file.rb +36 -0
  17. data/lib/holistic/document/location.rb +35 -0
  18. data/lib/holistic/document/unsaved/change.rb +24 -0
  19. data/lib/holistic/document/unsaved/collection.rb +21 -0
  20. data/lib/holistic/document/unsaved/record.rb +83 -0
  21. data/lib/holistic/extensions/events.rb +37 -0
  22. data/lib/holistic/extensions/ruby/stdlib.rb +43 -0
  23. data/lib/holistic/language_server/current.rb +11 -0
  24. data/lib/holistic/language_server/format/file_uri.rb +19 -0
  25. data/lib/holistic/language_server/lifecycle.rb +59 -0
  26. data/lib/holistic/language_server/message.rb +21 -0
  27. data/lib/holistic/language_server/protocol.rb +45 -0
  28. data/lib/holistic/language_server/request.rb +21 -0
  29. data/lib/holistic/language_server/requests/lifecycle/exit.rb +10 -0
  30. data/lib/holistic/language_server/requests/lifecycle/initialize.rb +75 -0
  31. data/lib/holistic/language_server/requests/lifecycle/initialized.rb +13 -0
  32. data/lib/holistic/language_server/requests/lifecycle/shutdown.rb +14 -0
  33. data/lib/holistic/language_server/requests/text_document/completion.rb +68 -0
  34. data/lib/holistic/language_server/requests/text_document/did_change.rb +30 -0
  35. data/lib/holistic/language_server/requests/text_document/did_close.rb +33 -0
  36. data/lib/holistic/language_server/requests/text_document/did_open.rb +16 -0
  37. data/lib/holistic/language_server/requests/text_document/did_save.rb +33 -0
  38. data/lib/holistic/language_server/requests/text_document/find_references.rb +52 -0
  39. data/lib/holistic/language_server/requests/text_document/go_to_definition.rb +64 -0
  40. data/lib/holistic/language_server/response.rb +39 -0
  41. data/lib/holistic/language_server/router.rb +48 -0
  42. data/lib/holistic/language_server/stdio/parser.rb +65 -0
  43. data/lib/holistic/language_server/stdio/server.rb +46 -0
  44. data/lib/holistic/language_server/stdio/start.rb +48 -0
  45. data/lib/holistic/ruby/autocompletion/suggest.rb +75 -0
  46. data/lib/holistic/ruby/parser/constant_resolution.rb +61 -0
  47. data/lib/holistic/ruby/parser/live_editing/process_file_changed.rb +62 -0
  48. data/lib/holistic/ruby/parser/nesting_syntax.rb +76 -0
  49. data/lib/holistic/ruby/parser/program_visitor.rb +205 -0
  50. data/lib/holistic/ruby/parser/table_of_contents.rb +17 -0
  51. data/lib/holistic/ruby/parser.rb +26 -0
  52. data/lib/holistic/ruby/reference/find_referenced_scope.rb +18 -0
  53. data/lib/holistic/ruby/reference/record.rb +13 -0
  54. data/lib/holistic/ruby/reference/register.rb +15 -0
  55. data/lib/holistic/ruby/reference/repository.rb +71 -0
  56. data/lib/holistic/ruby/reference/unregister.rb +11 -0
  57. data/lib/holistic/ruby/scope/kind.rb +11 -0
  58. data/lib/holistic/ruby/scope/list_references.rb +32 -0
  59. data/lib/holistic/ruby/scope/location.rb +43 -0
  60. data/lib/holistic/ruby/scope/outline.rb +52 -0
  61. data/lib/holistic/ruby/scope/record.rb +52 -0
  62. data/lib/holistic/ruby/scope/register.rb +31 -0
  63. data/lib/holistic/ruby/scope/repository.rb +49 -0
  64. data/lib/holistic/ruby/scope/unregister.rb +27 -0
  65. data/lib/holistic/ruby/type_inference/clue/method_call.rb +15 -0
  66. data/lib/holistic/ruby/type_inference/clue/scope_reference.rb +13 -0
  67. data/lib/holistic/ruby/type_inference/conclusion.rb +20 -0
  68. data/lib/holistic/ruby/type_inference/solve.rb +110 -0
  69. data/lib/holistic/ruby/type_inference/solve_pending_references.rb +13 -0
  70. data/lib/holistic/version.rb +5 -0
  71. data/lib/holistic.rb +27 -0
  72. metadata +158 -0
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ # Lifecycle constraints:
5
+ #
6
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize
7
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#shutdown
8
+ class Lifecycle
9
+ UnexpectedStateError = ::Class.new(::StandardError)
10
+
11
+ attr_reader :state
12
+
13
+ def initialize
14
+ @state = :waiting_initialize_event
15
+ end
16
+
17
+ def waiting_initialized_event!
18
+ if @state != :waiting_initialize_event
19
+ raise UnexpectedStateError, "state must be :waiting_initialize_event, got: #{@state.inspect}"
20
+ end
21
+
22
+ @state = :waiting_initialized_event
23
+ end
24
+
25
+ def initialized!
26
+ if @state != :waiting_initialized_event
27
+ raise UnexpectedStateError, "state must be :waiting_initialized_event, got: #{@state.inspect}"
28
+ end
29
+
30
+ @state = :initialized
31
+ end
32
+
33
+ def shutdown!
34
+ if @state != :initialized
35
+ raise UnexpectedStateError, "state must be :initialized, got: #{@state.inspect}"
36
+ end
37
+
38
+ @state = :shutdown
39
+ end
40
+
41
+ def initialized?
42
+ @state == :initialized || @state == :shutdown
43
+ end
44
+
45
+ def accept?(method)
46
+ return true if method == "exit"
47
+ return true if method == "initialize" && @state == :waiting_initialize_event
48
+ return true if method == "initialized" && @state == :waiting_initialized_event
49
+
50
+ return method != "initialize" && method != "initialized" if @state == :initialized
51
+
52
+ false
53
+ end
54
+
55
+ def reject?(method)
56
+ !accept?(method)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ Message = ::Data.define(:data) do
5
+ def params
6
+ data["params"]
7
+ end
8
+
9
+ def param(*keys)
10
+ data["params"].dig(*keys)
11
+ end
12
+
13
+ def method
14
+ data["method"]
15
+ end
16
+
17
+ def id
18
+ data["id"]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer::Protocol
4
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#abstractMessage
5
+ JSONRPC_VERSION = "2.0"
6
+
7
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#headerPart
8
+ END_OF_HEADER = "\r\n\r\n"
9
+ CONTENT_LENGTH_HEADER = "Content-Length"
10
+
11
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_synchronization
12
+ INCREMENTAL_TEXT_DOCUMENT_SYNCHRONIZATION = 2
13
+
14
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage
15
+ REQUEST_FAILED_ERROR_CODE = -32803
16
+ INVALID_REQUEST_ERROR_CODE = -32600
17
+ SERVER_NOT_INITIALIZED_ERROR_CODE = -32002
18
+
19
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind
20
+ COMPLETION_ITEM_KIND_TEXT = 1
21
+ COMPLETION_ITEM_KIND_METHOD = 2
22
+ COMPLETION_ITEM_KIND_FUNCTION = 3
23
+ COMPLETION_ITEM_KIND_CONSTRUCTOR = 4
24
+ COMPLETION_ITEM_KIND_FIELD = 5
25
+ COMPLETION_ITEM_KIND_VARIABLE = 6
26
+ COMPLETION_ITEM_KIND_CLASS = 7
27
+ COMPLETION_ITEM_KIND_INTERFACE = 8
28
+ COMPLETION_ITEM_KIND_MODULE = 9
29
+ COMPLETION_ITEM_KIND_PROPERTY = 10
30
+ COMPLETION_ITEM_KIND_UNIT = 11
31
+ COMPLETION_ITEM_KIND_VALUE = 12
32
+ COMPLETION_ITEM_KIND_ENUM = 13
33
+ COMPLETION_ITEM_KIND_KEYWORD = 14
34
+ COMPLETION_ITEM_KIND_SNIPPET = 15
35
+ COMPLETION_ITEM_KIND_COLOR = 16
36
+ COMPLETION_ITEM_KIND_FILE = 17
37
+ COMPLETION_ITEM_KIND_REFERENCE = 18
38
+ COMPLETION_ITEM_KIND_FOLDER = 19
39
+ COMPLETION_ITEM_KIND_ENUM_MEMBER = 20
40
+ COMPLETION_ITEM_KIND_CONSTANT = 21
41
+ COMPLETION_ITEM_KIND_STRUCT = 22
42
+ COMPLETION_ITEM_KIND_EVENT = 23
43
+ COMPLETION_ITEM_KIND_OPERATOR = 24
44
+ COMPLETION_ITEM_KIND_TYPE_PARAMTER = 25
45
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ Request = ::Struct.new(:message, :application, :response, keyword_init: true) do
5
+ def respond_with(result)
6
+ self.response = Response::Success.new(message_id: message.id, result:)
7
+ end
8
+
9
+ def respond_with_error(code:, description: nil, data: nil)
10
+ self.response = Response::Error.new(message_id: message.id, code:, message: description, data:)
11
+ end
12
+
13
+ def drop
14
+ self.response = Response::Drop.new
15
+ end
16
+
17
+ def param(*keys)
18
+ message.param(*keys)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#exit
5
+ module Requests::Lifecycle::Exit
6
+ extend self
7
+
8
+ def call(_request) = Response::Exit.new
9
+ end
10
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize
5
+ module Requests::Lifecycle::Initialize
6
+ extend self
7
+
8
+ # TODO: support multiple workspace directories.
9
+
10
+ def call(request)
11
+ application = create_application(request)
12
+
13
+ advance_lifecycle_state
14
+
15
+ parse_application_in_background(application)
16
+
17
+ respond_with_holistic_capabilities(request)
18
+ end
19
+
20
+ private
21
+
22
+ def create_application(request)
23
+ ::Holistic.logger.info("===========")
24
+ ::Holistic.logger.info(request.message.inspect)
25
+
26
+ root_directory = request.param("rootPath")
27
+ name = ::File.basename(root_directory)
28
+
29
+ Current.application = ::Holistic::Application.new(name:, root_directory:)
30
+
31
+ ::Holistic::Extensions::Ruby::Stdlib.register(Current.application)
32
+
33
+ Current.application
34
+ end
35
+
36
+ def advance_lifecycle_state
37
+ Current.lifecycle.waiting_initialized_event!
38
+ end
39
+
40
+ def parse_application_in_background(application)
41
+ ::Holistic::BackgroundProcess.run do
42
+ ::Holistic::Ruby::Parser::ParseDirectory.call(application:, directory_path: application.root_directory)
43
+
44
+ ::Holistic::Ruby::TypeInference::SolvePendingReferences.call(application:)
45
+ end
46
+ end
47
+
48
+ def respond_with_holistic_capabilities(request)
49
+ request.respond_with({
50
+ capabilities: {
51
+ # Defines how the host (editor) should sync document changes to the language server.
52
+ # Incremental means documents are synced by sending the full content on open.
53
+ # After that only incremental updates to the document are sent.
54
+ textDocumentSync: Protocol::INCREMENTAL_TEXT_DOCUMENT_SYNCHRONIZATION,
55
+
56
+ # The server provides goto definition support.
57
+ definitionProvider: true,
58
+
59
+ # The server provides find references support.
60
+ referencesProvider: true,
61
+
62
+ # The server provides completion support
63
+ completionProvider: {
64
+ triggerCharacters: [".", "@"],
65
+ resolveProvider: true
66
+ }
67
+ },
68
+ serverInfo: {
69
+ name: "Holistic Ruby",
70
+ version: ::Holistic::VERSION
71
+ }
72
+ })
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ module Requests::Lifecycle::Initialized
5
+ extend self
6
+
7
+ def call(request)
8
+ Current.lifecycle.initialized!
9
+
10
+ request.drop
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#shutdown
5
+ module Requests::Lifecycle::Shutdown
6
+ extend self
7
+
8
+ def call(request)
9
+ Current.lifecycle.shutdown!
10
+
11
+ request.respond_with(nil)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ module Requests::TextDocument::Completion
5
+ extend self
6
+
7
+ def call(request)
8
+ cursor = build_cursor_from_request_params(request)
9
+
10
+ document = request.application.unsaved_documents.find(cursor.file_path)
11
+
12
+ return request.respond_with(nil) if document.nil?
13
+
14
+ if document.has_unsaved_changes?
15
+ ::Holistic::Ruby::Parser::LiveEditing::ProcessFileChanged.call(
16
+ application: request.application,
17
+ file: document.to_file
18
+ )
19
+ end
20
+
21
+ code = document.expand_code(cursor)
22
+ scope = request.application.scopes.find_inner_most_scope_by_cursor(cursor) || request.application.root_scope
23
+
24
+ return request.respond_with(nil) if code.blank?
25
+
26
+ suggestions = ::Holistic::Ruby::Autocompletion::Suggest.call(code:, scope:)
27
+
28
+ respond_with_suggestions(request, suggestions)
29
+ end
30
+
31
+ private
32
+
33
+ def build_cursor_from_request_params(request)
34
+ file_path = Format::FileUri.extract_path(request.param("textDocument", "uri"))
35
+ line = request.param("position", "line")
36
+ column = request.param("position", "character")
37
+
38
+ ::Holistic::Document::Cursor.new(file_path:, line:, column:)
39
+ end
40
+
41
+ module CompletionKind
42
+ FROM_SCOPE_TO_COMPLETION = {
43
+ ::Holistic::Ruby::Scope::Kind::CLASS => Protocol::COMPLETION_ITEM_KIND_CLASS,
44
+ ::Holistic::Ruby::Scope::Kind::LAMBDA => Protocol::COMPLETION_ITEM_KIND_FUNCTION,
45
+ ::Holistic::Ruby::Scope::Kind::METHOD => Protocol::COMPLETION_ITEM_KIND_METHOD,
46
+ ::Holistic::Ruby::Scope::Kind::MODULE => Protocol::COMPLETION_ITEM_KIND_MODULE,
47
+ ::Holistic::Ruby::Scope::Kind::ROOT => Protocol::COMPLETION_ITEM_KIND_MODULE
48
+ }.freeze
49
+
50
+ DEFAULT = Protocol::COMPLETION_ITEM_KIND_MODULE
51
+
52
+ def self.fetch(scope_kind)
53
+ FROM_SCOPE_TO_COMPLETION.fetch(scope_kind, DEFAULT)
54
+ end
55
+ end
56
+
57
+ def respond_with_suggestions(request, suggestions)
58
+ formatted_suggestions = suggestions.map do |suggestion|
59
+ {
60
+ label: suggestion.code,
61
+ kind: CompletionKind.fetch(suggestion.kind)
62
+ }
63
+ end
64
+
65
+ request.respond_with(formatted_suggestions)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ module Requests::TextDocument::DidChange
5
+ extend self
6
+
7
+ BuildDocumentChange = ->(params) do
8
+ ::Holistic::Document::Unsaved::Change.new(
9
+ range_length: params.dig("rangeLength"),
10
+ text: params.dig("text"),
11
+ start_line: params.dig("range", "start", "line"),
12
+ start_column: params.dig("range", "start", "character"),
13
+ end_line: params.dig("range", "end", "line"),
14
+ end_column: params.dig("range", "end", "character")
15
+ )
16
+ end
17
+
18
+ def call(request)
19
+ file_path = Format::FileUri.extract_path(request.param("textDocument", "uri"))
20
+
21
+ unsaved_document = request.application.unsaved_documents.find(file_path)
22
+
23
+ request.param("contentChanges").map(&BuildDocumentChange).each do |change|
24
+ unsaved_document.apply_change(change)
25
+ end
26
+
27
+ request.respond_with(nil)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ module Requests::TextDocument::DidClose
5
+ extend self
6
+
7
+ def call(request)
8
+ path = Format::FileUri.extract_path(request.message.param("textDocument", "uri"))
9
+
10
+ unsaved_document = request.application.unsaved_documents.find(path)
11
+
12
+ if unsaved_document.present?
13
+ request.application.unsaved_documents.delete(path)
14
+
15
+ if unsaved_document.has_unsaved_changes?
16
+ unsaved_document.restore_original_content!
17
+
18
+ process_in_background(application: request.application, file: unsaved_document.to_file)
19
+ end
20
+ end
21
+
22
+ request.respond_with(nil)
23
+ end
24
+
25
+ private
26
+
27
+ def process_in_background(application:, file:)
28
+ ::Holistic::BackgroundProcess.run do
29
+ ::Holistic::Ruby::Parser::LiveEditing::ProcessFileChanged.call(application:, file:)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ module Requests::TextDocument::DidOpen
5
+ extend self
6
+
7
+ def call(request)
8
+ path = Format::FileUri.extract_path(request.message.param("textDocument", "uri"))
9
+ content = request.message.param("textDocument", "text")
10
+
11
+ request.application.unsaved_documents.add(path:, content:)
12
+
13
+ request.respond_with(nil)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ module Requests::TextDocument::DidSave
5
+ extend self
6
+
7
+ def call(request)
8
+ file_path = Format::FileUri.extract_path(request.param("textDocument", "uri"))
9
+ unsaved_document = request.application.unsaved_documents.find(file_path)
10
+
11
+ if unsaved_document.nil?
12
+ return request.respond_with_error(
13
+ code: Protocol::REQUEST_FAILED_ERROR_CODE,
14
+ description: "could not find document #{file_path} in the unsaved documents list"
15
+ )
16
+ end
17
+
18
+ unsaved_document.mark_as_saved!
19
+
20
+ process_in_background(application: request.application, file: unsaved_document.to_file)
21
+
22
+ request.respond_with(nil)
23
+ end
24
+
25
+ private
26
+
27
+ def process_in_background(application:, file:)
28
+ ::Holistic::BackgroundProcess.run do
29
+ ::Holistic::Ruby::Parser::LiveEditing::ProcessFileChanged.call(application:, file:)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references
5
+ module Requests::TextDocument::FindReferences
6
+ extend self
7
+
8
+ def call(request)
9
+ cursor = build_cursor_from_params(request)
10
+
11
+ request.application.unsaved_documents.find(cursor.file_path)&.then do |unsaved_document|
12
+ if unsaved_document.has_unsaved_changes?
13
+ ::Holistic::Ruby::Parser::LiveEditing::ProcessFileChanged.call(
14
+ application: request.application,
15
+ file: unsaved_document.to_file
16
+ )
17
+ end
18
+ end
19
+
20
+ case ::Holistic::Ruby::Scope::ListReferences.call(application: request.application, cursor:)
21
+ in :not_found
22
+ request.respond_with(nil)
23
+ in [:references_listed, {references:}]
24
+ respond_with_locations(request, references)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def build_cursor_from_params(request)
31
+ file_path = Format::FileUri.extract_path(request.param("textDocument", "uri"))
32
+ line = request.param("position", "line")
33
+ column = request.param("position", "character")
34
+
35
+ ::Holistic::Document::Cursor.new(file_path:, line:, column:)
36
+ end
37
+
38
+ def respond_with_locations(request, references)
39
+ locations = references.map do |reference|
40
+ {
41
+ "uri" => Format::FileUri.from_path(reference.location.file_path),
42
+ "range" => {
43
+ "start" => { "line" => reference.location.start_line, "character" => reference.location.start_column },
44
+ "end" => { "line" => reference.location.end_line, "character" => reference.location.end_column }
45
+ }
46
+ }
47
+ end
48
+
49
+ request.respond_with(locations)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ # https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition
5
+ module Requests::TextDocument::GoToDefinition
6
+ extend self
7
+
8
+ def call(request)
9
+ cursor = build_cursor_from_params(request)
10
+
11
+ request.application.unsaved_documents.find(cursor.file_path)&.then do |unsaved_document|
12
+ if unsaved_document.has_unsaved_changes?
13
+ ::Holistic::Ruby::Parser::LiveEditing::ProcessFileChanged.call(
14
+ application: request.application,
15
+ file: unsaved_document.to_file
16
+ )
17
+ end
18
+ end
19
+
20
+ case ::Holistic::Ruby::Reference::FindReferencedScope.call(application: request.application, cursor:)
21
+ in :not_found
22
+ request.respond_with(nil)
23
+ in :could_not_find_referenced_scope
24
+ request.respond_with(nil)
25
+ in [:referenced_scope_found, {reference:, referenced_scope:}]
26
+ respond_with_location_link(request, reference, referenced_scope)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def build_cursor_from_params(request)
33
+ file_path = Format::FileUri.extract_path(request.param("textDocument", "uri"))
34
+ line = request.param("position", "line")
35
+ column = request.param("position", "character")
36
+
37
+ ::Holistic::Document::Cursor.new(file_path:, line:, column:)
38
+ end
39
+
40
+ def respond_with_location_link(request, reference, referenced_scope)
41
+ origin_location = reference.location
42
+ target_declaration_location = referenced_scope.locations.main.declaration
43
+
44
+ location_link = {
45
+ "originSelectionRange" => {
46
+ "start" => { "line" => origin_location.start_line, "character" => origin_location.start_column },
47
+ "end" => { "line" => origin_location.end_line, "character" => origin_location.end_column }
48
+ },
49
+ "targetUri" => Format::FileUri.from_path(target_declaration_location.file_path),
50
+ "targetRange" => {
51
+ "start" => { "line" => target_declaration_location.start_line, "character" => target_declaration_location.start_column },
52
+ "end" => { "line" => target_declaration_location.end_line, "character" => target_declaration_location.end_column }
53
+ },
54
+ # TODO: store the location of the declaration name
55
+ "targetSelectionRange" => {
56
+ "start" => { "line" => target_declaration_location.start_line, "character" => target_declaration_location.start_column },
57
+ "end" => { "line" => target_declaration_location.end_line, "character" => target_declaration_location.end_column }
58
+ }
59
+ }
60
+
61
+ request.respond_with([location_link])
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ module Response
5
+ extend self
6
+
7
+ Success = ::Data.define(:message_id, :result) do
8
+ def encode
9
+ encoded_payload = {
10
+ "jsonrpc" => Protocol::JSONRPC_VERSION,
11
+ "id" => message_id,
12
+ "result" => result
13
+ }.to_json
14
+
15
+ "#{Protocol::CONTENT_LENGTH_HEADER}:#{encoded_payload.bytesize}#{Protocol::END_OF_HEADER}#{encoded_payload}"
16
+ end
17
+ end
18
+
19
+ Error = ::Data.define(:message_id, :code, :message, :data) do
20
+ def encode
21
+ encoded_payload = {
22
+ "jsonrpc" => Protocol::JSONRPC_VERSION,
23
+ "id" => message_id,
24
+ "error" => {
25
+ "code" => code,
26
+ "message" => message,
27
+ "data" => data
28
+ }
29
+ }.to_json
30
+
31
+ "#{Protocol::CONTENT_LENGTH_HEADER}:#{encoded_payload.bytesize}#{Protocol::END_OF_HEADER}#{encoded_payload}"
32
+ end
33
+ end
34
+
35
+ Exit = ::Data.define
36
+ NotFound = ::Data.define
37
+ Drop = ::Data.define
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Holistic::LanguageServer
4
+ module Router
5
+ extend self
6
+
7
+ ROUTES = {
8
+ "initialize" => Requests::Lifecycle::Initialize,
9
+ "initialized" => Requests::Lifecycle::Initialized,
10
+ "shutdown" => Requests::Lifecycle::Shutdown,
11
+ "exit" => Requests::Lifecycle::Exit,
12
+ "textDocument/didOpen" => Requests::TextDocument::DidOpen,
13
+ "textDocument/didChange" => Requests::TextDocument::DidChange,
14
+ "textDocument/didSave" => Requests::TextDocument::DidSave,
15
+ "textDocument/didClose" => Requests::TextDocument::DidClose,
16
+ "textDocument/definition" => Requests::TextDocument::GoToDefinition,
17
+ "textDocument/references" => Requests::TextDocument::FindReferences,
18
+ "textDocument/completion" => Requests::TextDocument::Completion
19
+ }.freeze
20
+
21
+ def dispatch(message)
22
+ request = Request.new(message:, application: Current.application)
23
+
24
+ ::ActiveSupport::Notifications.instrument("holistic.language_server.request", request:) do
25
+ return respond_with_rejection(request) if Current.lifecycle.reject?(message.method)
26
+
27
+ handler = ROUTES[message.method]
28
+
29
+ return Response::NotFound.new if handler.nil?
30
+
31
+ handler.call(request)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def respond_with_rejection(request)
38
+ error_code =
39
+ if Current.lifecycle.initialized?
40
+ Protocol::INVALID_REQUEST_ERROR_CODE
41
+ else
42
+ Protocol::SERVER_NOT_INITIALIZED_ERROR_CODE
43
+ end
44
+
45
+ request.respond_with_error(code: error_code)
46
+ end
47
+ end
48
+ end