Skip to content
Draft
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
2 changes: 1 addition & 1 deletion source/language_service/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub struct DocumentStatusDiagnostic {
#[derive(Debug)]
pub struct DiagnosticUpdate {
pub uri: String,
pub version: Option<u32>,
pub version: Option<u32>, // No version if not open
pub errors: Vec<ErrorKind>,
}

Expand Down
120 changes: 120 additions & 0 deletions source/npm/qsharp/src/language-service/diagnosticsPublisher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import type { VSDiagnostic } from "../../lib/web/qsc_wasm.js";

/**
* Both configuration for DiagnosticsPublisher and hooks so that different editors
* (and tests) can implement key operations differently.
*/
export interface DiagnosticsPublisherImpl {
/** Called when diagnostics are ready to be displayed to the user. */
publish: (uri: string, diagnostics: VSDiagnostic[]) => void;
/** Called to schedule deferred work. */
schedule: (callback: () => void, delayMs: number) => () => void;
/** How long after the last edit to wait before publishing. */
delayMs: number;
/** Upper bound on how long sustained typing can withhold a publish. */
maxDelayMs: number;
}

interface PendingDiagnostics {
uri: string;
diagnostics: VSDiagnostic[];
}

/**
* Withholds diagnostics for the document the user is currently typing in, so squiggles
* don't churn on every character of a half-written token.
*
* The goal is to have a small delay when the user is idle but a cap on the maximum delay
* before squiggles are drawn.
*
* Only the active file is affected - everything else (e.g. closed files) still publishes
* ASAP.
*/
export class DiagnosticsPublisher {
// Active file (i.e. subject to delays)
private activeUri: string | undefined;
// Deferred diagnostics for `activeUri`
private pending: PendingDiagnostics | undefined;
private cancelIdle: (() => void) | undefined;
private cancelCap: (() => void) | undefined;

constructor(private readonly impl: DiagnosticsPublisherImpl) {}

/// Callback for when a document is edited
onEdit(uri: string | undefined) {
if (uri !== this.activeUri) {
// A pending entry belongs to the document the user just left, which will get no
// further edits to end its burst.
this.publishPending();
this.endBurst();
this.activeUri = uri;
}

// Indicates a non-QDK document (which is still interesting, since it changes the activeUri)
if (uri === undefined) {
return;
}

this.cancelIdle?.();
this.cancelIdle = this.impl.schedule(() => {
this.cancelIdle = undefined;
this.endBurst();
this.publishPending();
}, this.impl.delayMs);

// Deliberately not restarted per edit - that is what makes it a cap rather than a
// second debounce, so a long typing run still refreshes.
if (!this.cancelCap) {
this.cancelCap = this.impl.schedule(() => {
this.cancelCap = undefined;
this.publishPending();
}, this.impl.maxDelayMs);
}
}

onDiagnosticsUpdate(uri: string, diagnostics: VSDiagnostic[]) {
// Nothing is waiting on this result: either it's for another document, or the
// typing already stopped and the burst ended.
if (uri !== this.activeUri || !this.isBursting) {
this.impl.publish(uri, diagnostics);
return;
}

// Clearing the last error is the one result worth showing mid-token.
if (diagnostics.length === 0) {
this.pending = undefined;
this.impl.publish(uri, diagnostics);
return;
}

this.pending = { uri, diagnostics };
}

publishPending() {
const pending = this.pending;
this.pending = undefined;
if (pending) {
this.impl.publish(pending.uri, pending.diagnostics);
}
}

dispose() {
this.pending = undefined;
this.endBurst();
}

/** A burst runs from an edit until `delayMs` of quiet. */
private get isBursting() {
return this.cancelIdle !== undefined;
}

private endBurst() {
this.cancelIdle?.();
this.cancelIdle = undefined;
this.cancelCap?.();
this.cancelCap = undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ export class QSharpLanguageService implements ILanguageService {
Event;
event.detail = {
uri,
version: version ?? 0,
version: version ?? 0, // No version if not open
diagnostics,
};
this.eventHandler.dispatchEvent(event);
Expand Down
2 changes: 2 additions & 0 deletions source/npm/qsharp/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export * as utils from "./utils.js";
export { log } from "./log.js";
export { QscEventTarget } from "./compiler/events.js";
export { QdkDiagnostics } from "./diagnostics.js";
export { DiagnosticsPublisher } from "./language-service/diagnosticsPublisher.js";
export { default as samples } from "./samples.generated.js";
export { default as openqasm_samples } from "./openqasm-samples.generated.js";
export {
Expand All @@ -233,6 +234,7 @@ export type {
LanguageServiceEvent,
LanguageServiceTestCallablesEvent,
} from "./language-service/language-service.js";
export type { DiagnosticsPublisherImpl } from "./language-service/diagnosticsPublisher.js";
export type { ProjectLoader } from "./project.js";
export type { CircuitGroup as CircuitData } from "./data-structures/circuit.js";
export type { LogLevel } from "./log.js";
Loading
Loading