Class: Mdlint::Lsp::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/mdlint/lsp.rb,
sig/internal.rbs

Constant Summary collapse

SEVERITY =
{ error: 1, warning: 2, info: 3 }.freeze

Instance Method Summary collapse

Constructor Details

#initialize(input: $stdin, output: $stdout) ⇒ Server

Returns a new instance of Server.

Parameters:

  • input: (Object) (defaults to: $stdin)
  • output: (Object) (defaults to: $stdout)


11
12
13
14
15
16
# File 'lib/mdlint/lsp.rb', line 11

def initialize(input: $stdin, output: $stdout)
  @input = input
  @output = output
  @documents = {}
  @running = true
end

Instance Method Details

#code_actions(uri) ⇒ Object

Parameters:

  • uri (Object)

Returns:

  • (Object)


123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/mdlint/lsp.rb', line 123

def code_actions(uri)
  text = @documents[uri]
  return [] unless text

  fixed = Mdlint.fix(text)
  return [] if fixed == text

  [{
    title: "Fix Markdown",
    kind: "quickfix",
    isPreferred: true,
    edit: { changes: { uri => [{ range: full_range(text), newText: fixed }] } }
  }]
end

#formatting_edits(uri) ⇒ Object

Parameters:

  • uri (Object)

Returns:

  • (Object)


116
117
118
119
120
121
# File 'lib/mdlint/lsp.rb', line 116

def formatting_edits(uri)
  text = @documents[uri]
  return [] unless text

  [{ range: full_range(text), newText: Mdlint.format(text) }]
end

#full_range(text) ⇒ Object

Parameters:

  • text (Object)

Returns:

  • (Object)


158
159
160
161
162
163
# File 'lib/mdlint/lsp.rb', line 158

def full_range(text)
  lines = text.lines
  last_line = [lines.length - 1, 0].max
  last_character = lines.last ? lines.last.chomp.length : 0
  { start: { line: 0, character: 0 }, end: { line: last_line, character: last_character } }
end

#handle_message(message) ⇒ Object

Parameters:

  • message (Object)

Returns:

  • (Object)


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/mdlint/lsp.rb', line 57

def handle_message(message)
  method = message["method"]
  params = message["params"] || {}
  id = message["id"]

  case method
  when "initialize"
    response(id, {
      capabilities: {
        textDocumentSync: 1,
        documentFormattingProvider: true,
        codeActionProvider: true
      },
      serverInfo: { name: "mdlint", version: Mdlint::VERSION }
    })
  when "initialized"
    nil
  when "shutdown"
    response(id, nil)
  when "exit"
    @running = false
    nil
  when "textDocument/didOpen"
    update_document(params.dig("textDocument", "uri"), params.dig("textDocument", "text"))
    publish_diagnostics(params.dig("textDocument", "uri"))
    nil
  when "textDocument/didChange"
    uri = params.dig("textDocument", "uri")
    change = Array(params["contentChanges"]).last
    text = change.is_a?(Hash) ? change["text"] : nil
    update_document(uri, text)
    publish_diagnostics(uri)
    nil
  when "textDocument/didClose"
    @documents.delete(params.dig("textDocument", "uri"))
    nil
  when "textDocument/formatting"
    response(id, formatting_edits(params.dig("textDocument", "uri")))
  when "textDocument/codeAction"
    response(id, code_actions(params.dig("textDocument", "uri")))
  else
    response(id, nil) if id
  end
end

#notification(method, params) ⇒ Object

Parameters:

  • method (Object)
  • params (Object)

Returns:

  • (Object)


106
107
108
# File 'lib/mdlint/lsp.rb', line 106

def notification(method, params)
  { jsonrpc: "2.0", method: method, params: params }
end

#publish_diagnostics(uri) ⇒ Object

Parameters:

  • uri (Object)

Returns:

  • (Object)


138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/mdlint/lsp.rb', line 138

def publish_diagnostics(uri)
  text = @documents[uri]
  return unless text

  diagnostics = Mdlint.lint(text, filename: uri_path(uri)).map do |violation|
    start_character = [violation.column.to_i - 1, 0].max
    {
      range: {
        start: { line: violation.line - 1, character: start_character },
        end: { line: violation.line - 1, character: start_character + 1 }
      },
      severity: SEVERITY.fetch(violation.severity, 2),
      code: violation.rule_id,
      source: "mdlint",
      message: violation.message
    }
  end
  write_message(notification("textDocument/publishDiagnostics", { uri: uri, diagnostics: diagnostics }))
end

#read_messageObject

Returns:

  • (Object)


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/mdlint/lsp.rb', line 31

def read_message
  headers = {}
  loop do
    line = @input.gets
    return nil unless line
    break if line == "\r\n" || line == "\n"

    key, value = line.split(":", 2)
    headers[key.downcase] = value.to_s.strip if key && value
  end
  length = headers.fetch("content-length").to_i
  payload = @input.read(length)
  return nil unless payload && payload.bytesize == length

  JSON.parse(payload)
rescue JSON::ParserError, KeyError
  nil
end

#response(id, result) ⇒ Object

Parameters:

  • id (Object)
  • result (Object)

Returns:

  • (Object)


102
103
104
# File 'lib/mdlint/lsp.rb', line 102

def response(id, result)
  { jsonrpc: "2.0", id: id, result: result }
end

#runObject

Returns:

  • (Object)


18
19
20
21
22
23
24
25
26
27
# File 'lib/mdlint/lsp.rb', line 18

def run
  while @running
    message = read_message
    break unless message

    response = handle_message(message)
    write_message(response) if response
  end
  0
end

#update_document(uri, text) ⇒ Object

Parameters:

  • uri (Object)
  • text (Object)

Returns:

  • (Object)


110
111
112
113
114
# File 'lib/mdlint/lsp.rb', line 110

def update_document(uri, text)
  return unless uri && text

  @documents[uri] = text
end

#uri_path(uri) ⇒ Object

Parameters:

  • uri (Object)

Returns:

  • (Object)


165
166
167
168
169
170
171
172
173
# File 'lib/mdlint/lsp.rb', line 165

def uri_path(uri)
  return nil unless uri
  return uri unless uri.start_with?("file:")

  path = URI.parse(uri).path
  URI.decode_www_form_component(path.to_s)
rescue URI::InvalidURIError
  uri
end

#write_message(message) ⇒ Object

Parameters:

  • message (Object)

Returns:

  • (Object)


50
51
52
53
54
55
# File 'lib/mdlint/lsp.rb', line 50

def write_message(message)
  payload = JSON.generate(message)
  @output.write("Content-Length: #{payload.bytesize}\r\n\r\n")
  @output.write(payload)
  @output.flush if @output.respond_to?(:flush)
end