Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,27 @@ struct VerilogServerOptions {
};
// namespace lsp

struct LSPServerOptions {
LSPServerOptions(bool disableDebounce = false, unsigned debounceMinMs = 200,
unsigned debounceMaxMs = 1500)
: disableDebounce(disableDebounce), debounceMinMs(debounceMinMs),
debounceMaxMs(debounceMaxMs) {}

/// Disable debouncing entirely (updates applied synchronously).
const bool disableDebounce;

/// Minimum debounce delay in milliseconds.
const unsigned debounceMinMs;

/// Maximum debounce delay in milliseconds.
/// A value of 0 means "no cap".
const unsigned debounceMaxMs;
};

/// Implementation for tools like `circt-verilog-lsp-server`.
llvm::LogicalResult
CirctVerilogLspServerMain(const VerilogServerOptions &options,
CirctVerilogLspServerMain(const LSPServerOptions &lspOptions,
const VerilogServerOptions &options,
llvm::lsp::JSONTransport &transport);

} // namespace lsp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
#include "llvm/Support/LSP/Transport.h"

llvm::LogicalResult circt::lsp::CirctVerilogLspServerMain(
const circt::lsp::LSPServerOptions &lspOptions,
const circt::lsp::VerilogServerOptions &options,
llvm::lsp::JSONTransport &transport) {
circt::lsp::VerilogServer server(options);
return circt::lsp::runVerilogLSPServer(server, transport);
return circt::lsp::runVerilogLSPServer(lspOptions, server, transport);
}
86 changes: 61 additions & 25 deletions lib/Tools/circt-verilog-lsp-server/LSPServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@
//===----------------------------------------------------------------------===//

#include "LSPServer.h"
#include "Utils/PendingChanges.h"
#include "VerilogServerImpl/VerilogServer.h"
#include "circt/Tools/circt-verilog-lsp-server/CirctVerilogLspServerMain.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/LSP/Protocol.h"
#include "llvm/Support/LSP/Transport.h"

#include <cstdint>
#include <mutex>
#include <optional>

#define DEBUG_TYPE "circt-verilog-lsp-server"
Expand All @@ -23,9 +28,13 @@ using namespace llvm::lsp;
//===----------------------------------------------------------------------===//

namespace {

struct LSPServer {
LSPServer(circt::lsp::VerilogServer &server, JSONTransport &transport)
: server(server), transport(transport) {}

LSPServer(const circt::lsp::LSPServerOptions &options,
circt::lsp::VerilogServer &server, JSONTransport &transport)
: server(server), transport(transport),
debounceOptions(circt::lsp::DebounceOptions::fromLSPOptions(options)) {}

//===--------------------------------------------------------------------===//
// Initialization
Expand Down Expand Up @@ -60,16 +69,34 @@ struct LSPServer {
circt::lsp::VerilogServer &server;
JSONTransport &transport;

/// An outgoing notification used to send diagnostics to the client when they
/// are ready to be processed.
OutgoingNotification<PublishDiagnosticsParams> publishDiagnostics;
/// A thread-safe version of `publishDiagnostics`
void sendDiagnostics(const PublishDiagnosticsParams &p) {
std::scoped_lock lk(diagnosticsMutex);
publishDiagnostics(p); // serialize the write
}

void
setPublishDiagnostics(OutgoingNotification<PublishDiagnosticsParams> diag) {
std::scoped_lock lk(diagnosticsMutex);
publishDiagnostics = std::move(diag);
}

/// Used to indicate that the 'shutdown' request was received from the
/// Language Server client.
bool shutdownRequestReceived = false;

private:
/// A mutex to serialize access to publishing diagnostics
std::mutex diagnosticsMutex;
/// An outgoing notification used to send diagnostics to the client when they
/// are ready to be processed.
OutgoingNotification<PublishDiagnosticsParams> publishDiagnostics;

circt::lsp::PendingChangesMap pendingChanges;
circt::lsp::DebounceOptions debounceOptions;
};
} // namespace

} // namespace
//===----------------------------------------------------------------------===//
// Initialization
//===----------------------------------------------------------------------===//
Expand Down Expand Up @@ -101,6 +128,7 @@ void LSPServer::onInitialize(const InitializeParams &params,
void LSPServer::onInitialized(const InitializedParams &) {}
void LSPServer::onShutdown(const NoParams &, Callback<std::nullptr_t> reply) {
shutdownRequestReceived = true;
pendingChanges.abort();
reply(nullptr);
}

Expand All @@ -115,10 +143,11 @@ void LSPServer::onDocumentDidOpen(const DidOpenTextDocumentParams &params) {
params.textDocument.version, diagParams.diagnostics);

// Publish any recorded diagnostics.
publishDiagnostics(diagParams);
sendDiagnostics(diagParams);
}

void LSPServer::onDocumentDidClose(const DidCloseTextDocumentParams &params) {
pendingChanges.erase(params.textDocument.uri);
std::optional<int64_t> version =
server.removeDocument(params.textDocument.uri);
if (!version)
Expand All @@ -127,18 +156,23 @@ void LSPServer::onDocumentDidClose(const DidCloseTextDocumentParams &params) {
// Empty out the diagnostics shown for this document. This will clear out
// anything currently displayed by the client for this document (e.g. in the
// "Problems" pane of VSCode).
publishDiagnostics(
PublishDiagnosticsParams(params.textDocument.uri, *version));
sendDiagnostics(PublishDiagnosticsParams(params.textDocument.uri, *version));
}

void LSPServer::onDocumentDidChange(const DidChangeTextDocumentParams &params) {
PublishDiagnosticsParams diagParams(params.textDocument.uri,
params.textDocument.version);
server.updateDocument(params.textDocument.uri, params.contentChanges,
params.textDocument.version, diagParams.diagnostics);

// Publish any recorded diagnostics.
publishDiagnostics(diagParams);
pendingChanges.debounceAndUpdate(
params, debounceOptions,
[this, params](std::unique_ptr<circt::lsp::PendingChanges> result) {
if (!result)
return; // obsolete

PublishDiagnosticsParams diagParams(params.textDocument.uri,
result->version);
server.updateDocument(params.textDocument.uri, result->changes,
result->version, diagParams.diagnostics);

sendDiagnostics(diagParams);
});
}

//===----------------------------------------------------------------------===//
Expand All @@ -163,11 +197,18 @@ void LSPServer::onReference(const ReferenceParams &params,
// Entry Point
//===----------------------------------------------------------------------===//

LogicalResult circt::lsp::runVerilogLSPServer(VerilogServer &server,
JSONTransport &transport) {
LSPServer lspServer(server, transport);
LogicalResult
circt::lsp::runVerilogLSPServer(const circt::lsp::LSPServerOptions &options,
VerilogServer &server,
JSONTransport &transport) {
LSPServer lspServer(options, server, transport);
MessageHandler messageHandler(transport);

// Diagnostics
lspServer.setPublishDiagnostics(
messageHandler.outgoingNotification<PublishDiagnosticsParams>(
"textDocument/publishDiagnostics"));

// Initialization
messageHandler.method("initialize", &lspServer, &LSPServer::onInitialize);
messageHandler.notification("initialized", &lspServer,
Expand All @@ -179,20 +220,15 @@ LogicalResult circt::lsp::runVerilogLSPServer(VerilogServer &server,
&LSPServer::onDocumentDidOpen);
messageHandler.notification("textDocument/didClose", &lspServer,
&LSPServer::onDocumentDidClose);

messageHandler.notification("textDocument/didChange", &lspServer,
&LSPServer::onDocumentDidChange);

// Definitions and References
messageHandler.method("textDocument/definition", &lspServer,
&LSPServer::onGoToDefinition);
messageHandler.method("textDocument/references", &lspServer,
&LSPServer::onReference);

// Diagnostics
lspServer.publishDiagnostics =
messageHandler.outgoingNotification<PublishDiagnosticsParams>(
"textDocument/publishDiagnostics");

// Run the main loop of the transport.
if (Error error = transport.run(messageHandler)) {
Logger::error("Transport error: {0}", error);
Expand Down
5 changes: 3 additions & 2 deletions lib/Tools/circt-verilog-lsp-server/LSPServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ class JSONTransport;
namespace circt {
namespace lsp {
class VerilogServer;

struct LSPServerOptions;
/// Run the main loop of the LSP server using the given Verilog server and
/// transport.
llvm::LogicalResult runVerilogLSPServer(VerilogServer &server,
llvm::LogicalResult runVerilogLSPServer(const LSPServerOptions &lspOptions,
VerilogServer &server,
llvm::lsp::JSONTransport &transport);

} // namespace lsp
Expand Down
7 changes: 7 additions & 0 deletions lib/Tools/circt-verilog-lsp-server/Utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@

add_circt_library(CIRCTVerilogLspServerUtils
LSPUtils.cpp
PendingChanges.cpp

LINK_LIBS PUBLIC
MLIRLspServerSupportLib
)

# An interface library target to expose headers to unittests
add_library(CIRCTVerilogLspUtilsHeaders INTERFACE)
target_include_directories(CIRCTVerilogLspUtilsHeaders INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}
)
146 changes: 146 additions & 0 deletions lib/Tools/circt-verilog-lsp-server/Utils/PendingChanges.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "PendingChanges.h"

namespace circt {
namespace lsp {

/// Factory: build from server options. Keep mapping 1:1 for clarity.
DebounceOptions
DebounceOptions::fromLSPOptions(const circt::lsp::LSPServerOptions &opts) {
DebounceOptions d;
d.disableDebounce = opts.disableDebounce;
d.debounceMinMs = opts.debounceMinMs;
d.debounceMaxMs = opts.debounceMaxMs;
return d;
}

void PendingChangesMap::abort() {
std::scoped_lock lock(mu);
pending.clear();
pool.wait();
}

void PendingChangesMap::erase(llvm::StringRef key) {
std::scoped_lock lock(mu);
pending.erase(key);
}

void PendingChangesMap::erase(const llvm::lsp::URIForFile &uri) {
auto file = uri.file();
if (!file.empty())
erase(file);
}

void PendingChangesMap::debounceAndUpdate(
const llvm::lsp::DidChangeTextDocumentParams &params,
DebounceOptions options,
std::function<void(std::unique_ptr<PendingChanges>)> cb) {
enqueueChange(params);
debounceAndThen(params, options, std::move(cb));
}

void PendingChangesMap::enqueueChange(
const llvm::lsp::DidChangeTextDocumentParams &params) {
// Key by normalized LSP file path. If your pipeline allows multiple
// spellings (symlinks/case), normalize upstream or canonicalize here.
const auto now = std::chrono::steady_clock::now();
const std::string key = params.textDocument.uri.file().str();

std::scoped_lock lock(mu);
PendingChanges &pending = getOrCreateEntry(key);

pending.changes.insert(pending.changes.end(), params.contentChanges.begin(),
params.contentChanges.end());
pending.version = params.textDocument.version;
pending.lastChangeTime = now;

// If this was the first insert after a flush, record start of burst.
if (pending.changes.size() == params.contentChanges.size())
pending.firstChangeTime = now;
}

void PendingChangesMap::debounceAndThen(
const llvm::lsp::DidChangeTextDocumentParams &params,
DebounceOptions options,
std::function<void(std::unique_ptr<PendingChanges>)> cb) {
const std::string key = params.textDocument.uri.file().str();
const auto scheduleTime = std::chrono::steady_clock::now();

// If debounce is disabled, run on main thread
if (options.disableDebounce) {
std::scoped_lock lock(mu);
auto it = pending.find(key);
if (it == pending.end())
return cb(nullptr);
return cb(takeAndErase(it));
}

// If debounced, run entirely on the pool; do not block the LSP thread.
tasks.async([this, key, scheduleTime, options, cb = std::move(cb)]() {
// Simple timer: sleep min-quiet before checking. We rely on the fact
// that newer edits can arrive while we sleep, updating lastChangeTime.
if (options.debounceMinMs > 0)
std::this_thread::sleep_for(
std::chrono::milliseconds(options.debounceMinMs));

std::unique_ptr<PendingChanges>
result; // decided under lock, callback after

{
std::scoped_lock lock(mu);
auto it = pending.find(key);
if (it != pending.end()) {
PendingChanges &pc = it->second;
const auto now = std::chrono::steady_clock::now();

// quietSinceSchedule: if no newer edits arrived after we scheduled
// this task, then we consider the burst "quiet" and flush now.
const bool quietSinceSchedule = (pc.lastChangeTime <= scheduleTime);

// Apply max-burst cap if configured: force a flush once the total
// time since first change exceeds the cap.
bool maxWaitExpired = false;
if (options.debounceMaxMs > 0) {
const auto elapsedMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
now - pc.firstChangeTime)
.count();
maxWaitExpired =
static_cast<uint64_t>(elapsedMs) >= options.debounceMaxMs;
}

if (quietSinceSchedule || maxWaitExpired)
result = takeAndErase(it); // flush now
// else: newer edits arrived; obsolete -> result stays null
}
}

// Invoke outside the lock to avoid deadlocks and allow heavy work.
cb(std::move(result)); // nullptr => obsolete (no flush)
});
}

PendingChanges &PendingChangesMap::getOrCreateEntry(std::string_view key) {
auto it = pending.find(key);
if (it != pending.end())
return it->second;
auto inserted = pending.try_emplace(key);
return inserted.first->second;
}

std::unique_ptr<PendingChanges>
PendingChangesMap::takeAndErase(llvm::StringMap<PendingChanges>::iterator it) {
auto out = std::make_unique<PendingChanges>(std::move(it->second));
pending.erase(it);
return out;
}

} // namespace lsp
} // namespace circt
Loading