holistic-ruby 0.1.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 (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