Class: Chutney::LSP::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/chutney/lsp/server.rb

Overview

A minimalistic language server which will lint gherkin files on open and save

Constant Summary collapse

LSP_CONST =
LanguageServer::Protocol::Constant
LSP_IO =
LanguageServer::Protocol::Transport::Stdio
LSP_IF =
LanguageServer::Protocol::Interface

Instance Method Summary collapse

Constructor Details

#initializeServer

Returns a new instance of Server.



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/chutney/lsp/server.rb', line 12

def initialize
  @writer = LSP_IO::Writer.new
  @reader = LSP_IO::Reader.new
  @mutex = Mutex.new
  @incoming_queue = Thread::Queue.new
  @outgoing_queue = Thread::Queue.new

  @dispatcher = Thread.new do
    while (message = @outgoing_queue.pop)
      if message.is_a? Result
        @mutex.synchronize { @writer.write(id: message.id, result: message.response) }
      else
        @mutex.synchronize { @writer.write(message.to_hash) }
      end
    end
  end

  @worker = Thread.new do
    while (message = @incoming_queue.pop)
      process_message(message)
    end
  end

  Thread.main.priority = 1
end

Instance Method Details

#diagnostic_message(file_uri, diagnostics) ⇒ Object



119
120
121
122
123
124
125
126
127
# File 'lib/chutney/lsp/server.rb', line 119

def diagnostic_message(file_uri, diagnostics)
  {
    method: 'textDocument/publishDiagnostics',
    params: {
      uri: file_uri,
      diagnostics:
    }
  }
end

#process_message(message) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/chutney/lsp/server.rb', line 38

def process_message(message)
  case message[:method]
  when 'initialize'
    run_initialize(message)
  when 'initialized'
    run_initialized
  when 'textDocument/didOpen', 'textDocument/didSave'
    run_did_change(message)
  when 'textDocument/didClose'
    # no-op
  end
end

#run_did_change(message) ⇒ Object



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/chutney/lsp/server.rb', line 99

def run_did_change(message)
  document = message.dig(:params, :textDocument)
  filename = document[:uri].delete_prefix('file://')
  send_log("Evaluating #{filename}")
  linter = Chutney::ChutneyLint.new(*filename)
  linter.configuration.quiet!
  begin
    offenses = linter.analyse.values.first.filter { |r| r[:issues].any? }
  rescue StandardError => e
    send_log("Could not parse #{filename} as Gherkin. Received: #{e.full_message}", error: true)
    send_notification("Could not parse #{filename} as Gherkin.", error: true)
    return
  end
  send_log("Found #{offenses.count} offenses")
  diagnostics = offenses
                .flat_map { |group| group[:issues].each { |issue| issue[:linter] = group[:linter] } }
                .map { |offense| to_diagnostic(offense) }
  send_message(diagnostic_message(document[:uri], diagnostics))
end

#run_initialize(message) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/chutney/lsp/server.rb', line 74

def run_initialize(message)
  initialize_result = LSP_IF::InitializeResult.new(
    capabilities: LSP_IF::ServerCapabilities.new(
      document_formatting_provider: true,
      text_document_sync: LSP_IF::TextDocumentSyncOptions.new(
        change: LSP_CONST::TextDocumentSyncKind::FULL,
        open_close: true,
        save: true
      )
    ),
    server_info: {
      name: 'chutney-lsp',
      version: VERSION
    }
  )
  send_message(Result.new(id: message[:id], response: initialize_result))
  send_log('Initializing')
end

#run_initializedObject



93
94
95
96
97
# File 'lib/chutney/lsp/server.rb', line 93

def run_initialized
  RubyVM::YJIT.enable if defined?(RubyVM::YJIT.enable)
  send_notification('Chutney LSP Server up and running')
  send_log('Initialized')
end

#send_log(message, method: 'window/logMessage', error: false) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/chutney/lsp/server.rb', line 57

def send_log(message, method: 'window/logMessage', error: false)
  type = error ? LSP_CONST::MessageType::ERROR : LSP_CONST::MessageType::INFO
  notification = LSP_IF::NotificationMessage.new(
    method:,
    jsonrpc: '2.0',
    params: LSP_IF::ShowMessageParams.new(
      type:,
      message: "Chutney LSP [#{VERSION}]: #{message}"
    )
  )
  send_message(notification)
end

#send_message(message) ⇒ Object



51
52
53
54
55
# File 'lib/chutney/lsp/server.rb', line 51

def send_message(message)
  return if outgoing_queue.closed?

  outgoing_queue << message
end

#send_notification(message, error: false) ⇒ Object



70
71
72
# File 'lib/chutney/lsp/server.rb', line 70

def send_notification(message, error: false)
  send_log(message, method: 'window/showMessage', error:)
end

#shutdownObject



145
146
147
148
149
150
151
152
153
# File 'lib/chutney/lsp/server.rb', line 145

def shutdown
  incoming_queue.clear
  outgoing_queue.clear
  incoming_queue.close
  outgoing_queue.close
  worker.join
  dispatcher.join
  send_log('Shutdown complete')
end

#startObject



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/chutney/lsp/server.rb', line 155

def start
  reader.read do |message|
    method = message[:method]
    send_log("Received #{method}")

    case method
    when 'initialize', 'initialized', 'textDocument/didOpen', 'textDocument/didClose', 'textDocument/didSave'
      incoming_queue.push(message)
    when 'shutdown'
      shutdown
    when 'exit'
      mutex.synchronize do
        status = incoming_queue.closed? ? 0 : 1
        exit(status)
      end
    end
  end
end

#to_diagnostic(offense) ⇒ Object



129
130
131
132
133
134
# File 'lib/chutney/lsp/server.rb', line 129

def to_diagnostic(offense)
  code = offense[:linter]
  message = offense[:message]
  source = 'chutney'
  { code:, message:, source:, severity: 1, range: to_range(offense[:location]) }
end

#to_range(location) ⇒ Object



136
137
138
139
140
141
142
143
# File 'lib/chutney/lsp/server.rb', line 136

def to_range(location)
  return { start: { character: 0, line: 0 }, end: { character: 0, line: 0 } } unless location

  {
    start: { character: location.fetch(:column, 1) - 1, line: location.fetch(:line, 1) - 1 },
    end: { character: 0, line: location.fetch(:line, 1) }
  }
end