Module: Jade::LSP::Converters

Extended by:
Converters
Included in:
Converters
Defined in:
lib/jade/lsp/converters.rb

Constant Summary collapse

SEVERITY =
{ error: 1, warning: 2, info: 3, hint: 4 }.freeze
SYMBOL_KIND =
{
  function: 12,
  enum: 10,
  enum_member: 22,
  struct: 23,
  interface: 11,
}.freeze
HOVERABLE_SYMBOLS =
[
  Jade::Symbol::Function,
  Jade::Symbol::StdlibFunction,
  Jade::Symbol::InteropFunction,
  Jade::Symbol::InterfaceFunction,
  Jade::Symbol::Constructor,
  Jade::Symbol::Variant,
  Jade::Symbol::Union,
  Jade::Symbol::Struct,
].freeze
COMPLETION_KIND_SNIPPET =

LSP CompletionItemKind values we use.

15
INSERT_FORMAT_SNIPPET =

LSP InsertTextFormat: 1 = PlainText, 2 = Snippet (with tab stops).

2
INLAY_HINT_TYPE =

InlayHintKind: 1 = Type, 2 = Parameter

1
INLAY_HINT_PARAMETER =
2

Instance Method Summary collapse

Instance Method Details

#completion_itemsObject



68
69
70
71
72
73
74
75
76
77
78
# File 'lib/jade/lsp/converters.rb', line 68

def completion_items
  Snippets::ALL.map do |snippet|
    {
      label: snippet.label,
      kind: COMPLETION_KIND_SNIPPET,
      detail: snippet.detail,
      insertText: snippet.body,
      insertTextFormat: INSERT_FORMAT_SNIPPET,
    }
  end
end

#definition_for_path(path, registry, entry, source_root) ⇒ Object



53
54
55
56
# File 'lib/jade/lsp/converters.rb', line 53

def definition_for_path(path, registry, entry, source_root)
  innermost_resolved(path, registry, entry)
    .then { definition_location(it, registry, entry, source_root) }
end

#diagnostic_to_lsp(diagnostic) ⇒ Object



175
176
177
178
179
180
181
182
# File 'lib/jade/lsp/converters.rb', line 175

def diagnostic_to_lsp(diagnostic)
  {
    range: diagnostic.primary.then { span_to_range(it.source, it.span) },
    severity: SEVERITY.fetch(diagnostic.severity, 3),
    source: 'jade',
    message: diagnostic_message(diagnostic),
  }
end

#format_edits(text) ⇒ Object

Re-runs the parse + format pipeline on the buffer text. Returns nil if parsing fails (don’t overwrite the user’s broken buffer with garbage), an empty array if the text is already formatted, or a single whole-document TextEdit otherwise.



94
95
96
97
98
99
100
101
102
103
104
# File 'lib/jade/lsp/converters.rb', line 94

def format_edits(text)
  source = Jade::Source.new(uri: 'buffer', text:)

  case Jade::Parsing.parse(Jade::Lexer.tokenize(source), source:)
  in Jade::Ok([ast, comments])
    formatted = Jade::Formatter.format(ast, comments:, source:) + "\n"
    formatted == text ? [] : [whole_document_edit(source, formatted)]
  in Jade::Err
    nil
  end
end

#hover_for_path(path, registry, entry) ⇒ Object



58
59
60
61
62
63
64
65
66
# File 'lib/jade/lsp/converters.rb', line 58

def hover_for_path(path, registry, entry)
  path
    .reverse
    .lazy
    .filter_map { hover_for_node(it, registry, entry) }
    .first
rescue StandardError
  nil
end

#inlay_hints_for(entry, range_offsets) ⇒ Object



84
85
86
87
88
# File 'lib/jade/lsp/converters.rb', line 84

def inlay_hints_for(entry, range_offsets)
  return [] unless entry.env

  collect_inlay_hints(entry.ast, entry, range_offsets)
end

#lsp_uri(relative_path, source_root) ⇒ Object



193
194
195
196
197
198
# File 'lib/jade/lsp/converters.rb', line 193

def lsp_uri(relative_path, source_root)
  File
    .expand_path(File.join(source_root, relative_path))
    .then { URI::DEFAULT_PARSER.escape(it) }
    .then { "file://#{it}" }
end

#offset_to_position(source, offset) ⇒ Object

Byte offsets, not UTF-16 code units. Matches the ‘utf-8` position encoding negotiated at initialize; under the default `utf-16`, columns drift right by N for each multi-byte char on the line.



11
12
13
14
# File 'lib/jade/lsp/converters.rb', line 11

def offset_to_position(source, offset)
  line = source.line_starts.rindex { it <= offset } || 0
  { line:, character: offset - source.line_starts[line] }
end

#position_to_offset(source, line, character) ⇒ Object



16
17
18
# File 'lib/jade/lsp/converters.rb', line 16

def position_to_offset(source, line, character)
  source.line_starts[line] + character
end

#prepare_rename_for_path(path, registry, entry, offset) ⇒ Object



129
130
131
132
133
134
135
136
137
138
# File 'lib/jade/lsp/converters.rb', line 129

def prepare_rename_for_path(path, registry, entry, offset)
  node = innermost_resolvable_node(path, registry, entry)
  return nil unless node

  symbol = resolve_symbol(node, registry, entry)
  return nil unless renameable?(symbol, registry)

  identifier_span(node, symbol, entry.source)
    .then { it.cover?(offset) ? span_to_range(entry.source, it) : nil }
end

#references_for_path(path, registry, entry, source_root, include_declaration:) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
# File 'lib/jade/lsp/converters.rb', line 117

def references_for_path(
  path, registry, entry, source_root, include_declaration:
)
  symbol = innermost_resolved(path, registry, entry)
  return nil unless symbol

  refs = usage_locations(symbol, registry, source_root)
  return refs unless include_declaration

  refs + declaration_locations(symbol, registry, entry, source_root)
end

#relative_path(uri, source_root) ⇒ Object



184
185
186
187
188
189
190
191
# File 'lib/jade/lsp/converters.rb', line 184

def relative_path(uri, source_root)
  uri
    .sub(%r{\Afile://}, '')
    .then { URI::DEFAULT_PARSER.unescape(it) }
    .then { Pathname.new(it) }
    .then { it.relative_path_from(Pathname.new(source_root)) }
    .then { it.to_s }
end

#rename_for_path(path, registry, entry, source_root, new_name) ⇒ Object



140
141
142
143
144
145
146
147
148
# File 'lib/jade/lsp/converters.rb', line 140

def rename_for_path(path, registry, entry, source_root, new_name)
  symbol = innermost_resolved(path, registry, entry)
  return nil unless renameable?(symbol, registry)

  rename_edits(symbol, registry, entry, source_root)
    .group_by { it[:uri] }
    .transform_values { it.map { |e| { range: e[:range], newText: new_name } } }
    .then { { changes: it } }
end

#span_to_range(source, span) ⇒ Object



20
21
22
23
24
25
# File 'lib/jade/lsp/converters.rb', line 20

def span_to_range(source, span)
  {
    start: offset_to_position(source, span.begin),
    end:   offset_to_position(source, span.end),
  }
end

#to_document_symbol(node, source) ⇒ Object



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/jade/lsp/converters.rb', line 150

def to_document_symbol(node, source)
  case node
  in AST::FunctionDeclaration(name:, range:)
    document_symbol(name, :function, source, range)

  in AST::TypeDeclaration(name:, range:, variants:)
    document_symbol(
      name, :enum, source, range,
      children: variants.map { variant_symbol(it, source) },
    )

  in AST::StructDeclaration(name:, range:)
    document_symbol(name, :struct, source, range)

  in AST::InterfaceDeclaration(name:, range:, functions:)
    document_symbol(
      name, :interface, source, range,
      children: functions.map { interface_fn_symbol(it, source) },
    )

  else
    nil
  end
end

#whole_document_edit(source, new_text) ⇒ Object



106
107
108
109
110
111
112
113
114
115
# File 'lib/jade/lsp/converters.rb', line 106

def whole_document_edit(source, new_text)
  last_line = source.line_starts.size - 1
  {
    range: {
      start: { line: 0, character: 0 },
      end:   { line: last_line, character: source.text.bytesize - source.line_starts[last_line] },
    },
    newText: new_text,
  }
end