Class: MilkTea::LSP::Server

Inherits:
Object
  • Object
show all
Includes:
ServerCallHierarchy, ServerCodeActions, ServerCodeLens, ServerCompletion, ServerConfiguration, ServerDebugInfo, ServerDefinition, ServerDiagnosticsScheduling, ServerExecuteCommand, ServerFoldingRange, ServerFormatting, ServerHover, ServerInlayHints, ServerLifecycle, ServerLinkedEditingRange, ServerOnTypeFormatting, ServerProgress, ServerReferences, ServerRename, ServerSelectionRange, ServerSemanticTokens, ServerSignatureHelp, ServerTextDocuments, ServerTypeHierarchy, ServerUtilities
Defined in:
lib/milk_tea/lsp/server.rb,
lib/milk_tea/lsp/server/hover.rb,
lib/milk_tea/lsp/server/rename.rb,
lib/milk_tea/lsp/server/progress.rb,
lib/milk_tea/lsp/server/code_lens.rb,
lib/milk_tea/lsp/server/lifecycle.rb,
lib/milk_tea/lsp/server/utilities.rb,
lib/milk_tea/lsp/server/completion.rb,
lib/milk_tea/lsp/server/debug_info.rb,
lib/milk_tea/lsp/server/definition.rb,
lib/milk_tea/lsp/server/formatting.rb,
lib/milk_tea/lsp/server/references.rb,
lib/milk_tea/lsp/server/inlay_hints.rb,
lib/milk_tea/lsp/server/code_actions.rb,
lib/milk_tea/lsp/server/configuration.rb,
lib/milk_tea/lsp/server/folding_range.rb,
lib/milk_tea/lsp/server/call_hierarchy.rb,
lib/milk_tea/lsp/server/signature_help.rb,
lib/milk_tea/lsp/server/text_documents.rb,
lib/milk_tea/lsp/server/type_hierarchy.rb,
lib/milk_tea/lsp/server/execute_command.rb,
lib/milk_tea/lsp/server/selection_range.rb,
lib/milk_tea/lsp/server/semantic_tokens.rb,
lib/milk_tea/lsp/server/on_type_formatting.rb,
lib/milk_tea/lsp/server/linked_editing_range.rb,
lib/milk_tea/lsp/server/diagnostics_scheduling.rb

Overview

Main LSP server — JSON-RPC 2.0 message loop with full MVP feature set.

Enhancements implemented:

1. Hover         — real type signature from semantic facts
2. Goto Def      — token-based definition site navigation
3. Incremental   — textDocumentSync: 2 with range-based edits
4. Multi-file    — workspace indexing via workspace/symbol
5. Completion    — function/type/value completions from facts

Defined Under Namespace

Modules: ServerCallHierarchy, ServerCodeActions, ServerCodeLens, ServerCompletion, ServerConfiguration, ServerDebugInfo, ServerDefinition, ServerDiagnosticsScheduling, ServerExecuteCommand, ServerFoldingRange, ServerFormatting, ServerHover, ServerInlayHints, ServerLifecycle, ServerLinkedEditingRange, ServerOnTypeFormatting, ServerProgress, ServerReferences, ServerRename, ServerSelectionRange, ServerSemanticTokens, ServerSignatureHelp, ServerTextDocuments, ServerTypeHierarchy, ServerUtilities Classes: FakeWarning

Constant Summary collapse

SEMANTIC_TOKEN_TYPES =
%w[
  namespace type class enum interface struct typeParameter parameter variable property enumMember event
  function method macro keyword modifier comment string number regexp operator decorator
].freeze
SEMANTIC_TOKEN_MODIFIERS =
%w[
  declaration definition readonly static deprecated abstract async modification documentation defaultLibrary
].freeze
KEYWORD_TOKEN_TYPES =
Token::KEYWORDS.values.to_set.freeze
DEFAULT_LIBRARY_TYPE_NAMES =
Types::BUILTIN_TYPE_NAMES.to_set.freeze
BUILTIN_FUNCTION_NAMES =
%w[
  ref_of const_ptr_of ptr_of read fatal reinterpret array span zero default adapt get field_of callable_of has_attribute attribute_of attribute_arg fields_of members_of attributes_of
].to_set.freeze
BUILTIN_ASSOCIATED_HOOK_NAMES =
%w[hash equal order].to_set.freeze
BUILTIN_CALL_HOVER_INFO =
{
  'fatal' => {
    signature: 'builtin fatal(message) -> never',
    docs: '`fatal(message)` aborts execution with the provided message.'
  },
  'ref_of' => {
    signature: 'builtin ref_of(value) -> ref[T]',
    docs: '`ref_of(x)` borrows a safe lvalue as `ref[T]`.'
  },
  'const_ptr_of' => {
    signature: 'builtin const_ptr_of(value) -> const_ptr[T]',
    docs: '`const_ptr_of(x)` takes the address of a safe lvalue as `const_ptr[T]`.'
  },
  'ptr_of' => {
    signature: 'builtin ptr_of(value) -> ptr[T]',
    docs: '`ptr_of(x)` takes the address of a safe lvalue as `ptr[T]`.'
  },
  'read' => {
    signature: 'builtin read(value) -> T',
    docs: '`read(value)` projects a `ref[T]` or pointer-like value to its referent.'
  },
  'adapt' => {
    signature: 'builtin adapt[Interface](value) -> dyn[Interface]',
    docs: '`adapt[Interface](value)` constructs a `dyn[Interface]` runtime interface value. Compile-time verified: the value\'s type must implement the interface.'
  },
  'reinterpret' => {
    signature: 'builtin reinterpret[T](value) -> T',
    docs: '`reinterpret[T](value)` reinterprets the bits of `value` as type `T` without conversion. Requires `unsafe`.'
  },
  'array' => {
    signature: 'builtin array[T, N](elements...) -> array[T, N]',
    docs: '`array[T, N]` constructs a fixed-size array from elements. Omitted trailing elements default to zero.'
  },
  'span' => {
    signature: 'builtin span[T](data, len) -> span[T]',
    docs: '`span[T](data = ..., len = ...)` constructs a borrowed pointer-plus-length view over contiguous memory.'
  },
  'zero' => {
    signature: 'builtin zero[T] -> T',
    docs: '`zero[T]` returns a zero-initialized value of type `T`. All bits are set to zero.'
  },
  'default' => {
    signature: 'builtin default[T] -> T',
    docs: '`default[T]` calls `T.default()` to produce a semantic default value. Requires an accessible `default()` associated function on `T`.'
  },
  'get' => {
    signature: 'builtin get(collection, index) -> ptr[T]?',
    docs: '`get(collection, index)` performs recoverable bounds-checked indexing, returning `ptr[T]?` (null on out-of-bounds) instead of aborting.'
  },
  'attribute_arg' => {
    signature: 'builtin attribute_arg[T](handle) -> T',
    docs: '`attribute_arg[T](handle)` returns the `T`-typed argument of a resolved `attribute_handle`. The attribute must declare an argument of type `T`.'
  },
  'field_of' => {
    signature: 'builtin field_of(Type, field_name) -> field_handle',
    docs: '`field_of(Type, field_name)` returns a compile-time handle for the named field on a struct type.'
  },
  'callable_of' => {
    signature: 'builtin callable_of(name) -> callable_handle',
    docs: '`callable_of(name)` returns a compile-time handle for a callable declaration name.'
  },
  'has_attribute' => {
    signature: 'builtin has_attribute(target, attribute_name) -> bool',
    docs: '`has_attribute(target, attribute_name)` checks at compile time whether the resolved attribute is applied to the target.'
  },
  'attribute_of' => {
    signature: 'builtin attribute_of(target, attribute_name) -> attribute_handle',
    docs: '`attribute_of(target, attribute_name)` returns the applied attribute handle for the resolved target-and-attribute pair; use `has_attribute(...)` when absence is expected.'
  },
  'fields_of' => {
    signature: 'builtin fields_of(Type) -> array[field_handle, N]',
    docs: '`fields_of(Type)` returns all struct fields as a compile-time `array[field_handle, N]`, in declaration order. Use with `inline for` for reflective field iteration.'
  },
  'members_of' => {
    signature: 'builtin members_of(Type) -> array[member_handle, N]',
    docs: '`members_of(Type)` returns all members of an enum or flags type as a compile-time `array[member_handle, N]`. Each handle exposes `.name` and `.value`.'
  },
  'attributes_of' => {
    signature: 'builtin attributes_of(target [, name]) -> array[attribute_handle, N]',
    docs: '`attributes_of(target)` returns all attributes applied to a type, field, or callable as a compile-time array. `attributes_of(target, name)` filters by attribute kind.'
  },
}.freeze
OPERATOR_TOKEN_TYPES =
%i[
  amp colon comma caret dot lparen rparen pipe lbracket rbracket question
  equal plus minus star slash percent less greater tilde
  arrow shift_left shift_right plus_equal minus_equal star_equal slash_equal percent_equal
  amp_equal pipe_equal caret_equal shift_left_equal shift_right_equal
  equal_equal bang_equal less_equal greater_equal ellipsis
  at dot_dot
].to_set.freeze
DIAGNOSTICS_WORKER_COUNT =
Integer(ENV.fetch('MILK_TEA_LSP_DIAGNOSTICS_WORKERS', '2')).clamp(1, 8)

Constants included from ServerCompletion

ServerCompletion::COMPLETION_KEYWORDS, ServerCompletion::MAX_COMPLETION_ITEMS, ServerCompletion::TYPE_CONSTRUCTOR_KEYWORDS

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(protocol: Protocol) ⇒ Server

Returns a new instance of Server.



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/milk_tea/lsp/server.rb', line 197

def initialize(protocol: Protocol)
  @protocol = protocol
  @workspace = Workspace.new
  @format_mode = :tidy
  @dependency_resolution_mode = :auto
  @platform_override = nil
  @position_encoding = 'utf-16'
  @handlers = {}
  @diagnostic_report_cache = {}
  @workspace_diagnostic_cache = {}
  @semantic_tokens_cache = {}
  @semantic_tokens_delta_cache = {}
  @fixall_cache = {}
  @definition_file_token_cache = {}
  @definition_file_ast_cache = {}
  @diagnostics_perf = {
    scheduled: 0,
    skipped_unchanged: 0,
    cancelled: 0,
    dequeued: 0,
    collected_for_pull: 0,
    published: 0,
    dropped_stale: 0,
    requeued: 0,
    queue_peak: 0,
  }
  @diagnostics_mutex = Mutex.new
  @diagnostics_pending = {}
  @diagnostics_enqueued = Set.new
  @diagnostics_generation = Hash.new(0)
  @diagnostics_last_scheduled_hash = {}
  @diagnostics_queue = Queue.new
  @diagnostics_workers = []
  @cancelled_requests_mutex = Mutex.new
  @cancelled_request_ids = Set.new
  @_initialize_called = false
  @_indexing_thread = nil
  Diagnostics.protocol = @protocol
  Diagnostics.position_encoding = @position_encoding
  register_handlers
  start_diagnostics_workers
end

Class Method Details

.semantic_tokens_for_path(path, module_roots: nil, package_graph: nil) ⇒ Object



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/milk_tea/lsp/server.rb', line 156

def self.semantic_tokens_for_path(path, module_roots: nil, package_graph: nil)
  expanded_path = File.expand_path(path)
  roots = module_roots || MilkTea::ModuleRoots.roots_for_path(expanded_path)
  source = File.read(expanded_path)
  tokens = MilkTea::Lexer.lex(source, path: expanded_path)
  facts = MilkTea::ModuleLoader.new(
    module_roots: roots,
    package_graph: package_graph || load_package_graph(expanded_path),
  ).check_file(expanded_path)

  helper = allocate
  entries = helper.send(:build_semantic_token_entries, tokens, facts)
  data = helper.send(:encode_semantic_tokens, entries)

  {
    path: expanded_path,
    moduleName: facts.module_name,
    legend: {
      tokenTypes: SEMANTIC_TOKEN_TYPES,
      tokenModifiers: SEMANTIC_TOKEN_MODIFIERS,
    },
    data: data,
    entries: entries.map do |entry|
      {
        line: entry[:line],
        startChar: entry[:start_char],
        length: entry[:length],
        tokenType: entry[:type].to_s,
        modifiers: entry[:modifiers].map(&:to_s),
      }
    end,
  }
end

Instance Method Details

#named_argument_definition_location(uri, name, tokens, token_index, stages: nil) ⇒ Object



742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
# File 'lib/milk_tea/lsp/server/definition.rb', line 742

def named_argument_definition_location(uri, name, tokens, token_index, stages: nil)
  return nil unless named_argument_label_token?(tokens, token_index)

  callee_name = named_argument_callee_name(tokens, token_index)
  return nil unless callee_name

  facts = measure_perf_stage(stages, 'definition_facts') { @workspace.get_facts(uri) }
  return nil unless facts

  func = facts.functions[callee_name]
  unless func
    facts.methods.each_value do |methods|
      method = methods[callee_name] || methods["static:#{callee_name}"]
      if method
        func = method
        break
      end
    end
  end
  return nil unless func

  param = func.ast.params.find { |p| p.name == name }
  return nil unless param

  range = ast_name_range(name, param.line, param.column)
  return nil unless range

  {
    uri: uri,
    range: {
      start: { line: range[:line] - 1, character: range[:column] - 1 },
      end: { line: range[:line] - 1, character: range[:column] - 1 + name.length },
    },
  }
end

#named_argument_label_references(uri, name, tokens, token_index, include_declaration:, stages: nil) ⇒ Object



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# File 'lib/milk_tea/lsp/server/references.rb', line 403

def named_argument_label_references(uri, name, tokens, token_index, include_declaration:, stages: nil)
  return nil unless named_argument_label_token?(tokens, token_index)

  callee_name = named_argument_callee_name(tokens, token_index)
  return nil unless callee_name

  facts = measure_perf_stage(stages, 'nav_ref_facts') { @workspace.get_facts(uri) }
  return nil unless facts

  func = facts.functions[callee_name]
  unless func
    facts.methods.each_value do |methods|
      method = methods[callee_name] || methods["static:#{callee_name}"]
      if method
        func = method
        break
      end
    end
  end
  return nil unless func

  param = func.ast.params.find { |p| p.name == name }
  return nil unless param

  range = declaration_name_range(param)
  unless range
    range = ast_name_range(name, param.line, param.column)
  end
  return [] unless range

  declaration_location = {
    uri: uri,
    range: {
      start: { line: range[:line] - 1, character: range[:column] - 1 },
      end: { line: range[:line] - 1, character: range[:column] - 1 + name.length },
    },
  }

  refs = if include_declaration
           [declaration_location]
         else
           []
         end

  mock_token = Struct.new(:lexeme, :type, :line, :column).new(name, :identifier, param.line, param.column)
  scoped_refs = scoped_local_reference_locations(uri, mock_token, param.line - 1, param.column - 1, facts, include_declaration: false)
  if scoped_refs
    refs.concat(scoped_refs)
  else
    refs
  end
end

#runObject



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/milk_tea/lsp/server.rb', line 240

def run
  if perf_logging?
    mode = perf_verbose? ? 'verbose' : 'threshold'
    warn "[LSP perf] enabled mode=#{mode} threshold_ms=#{Workspace::PERF_LOG_THRESHOLD_MS}"
  end

  loop do
    message = @protocol.read_message
    break if message.nil?
    next if message.equal?(Protocol::INVALID_MESSAGE)

    process_message(message)
  end
rescue StandardError => e
  warn "Server error: #{e.message}"
  raise
end