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
11 changes: 10 additions & 1 deletion client/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { fileURLToPath } from "url";
import fromMem from "@peggyjs/from-mem";

const PEGGY_INPUT_SCHEME = "peggyjsin";
const ID = "peggyLanguageServer";

interface GrammarConfig {
name: string;
Expand Down Expand Up @@ -226,10 +227,18 @@ export function activate(context: ExtensionContext): void {
}
});

const configChanged = workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(ID)) {
debounceExecution.wait = workspace.getConfiguration(ID).get("debounceMS");
}
});
debounceExecution.wait = workspace.getConfiguration(ID).get("debounceMS");

context.subscriptions.push(
visibleChanged,
configChanged,
documents_changed,
peggy_output,
visibleChanged,
commands.registerTextEditorCommand("editor.peggyLive", async editor => {
if (editor.document.languageId !== "peggy") {
console.error(`Invalid document: ${editor.document.uri.toString()}`);
Expand Down
36 changes: 15 additions & 21 deletions common/debounce.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
export type AnyFunction = (...args: any[]) => any;
export type AnyAsyncFunction = (...args: any[]) => Promise<any>;

export type Debounced<T extends AnyAsyncFunction>
= (...args: Parameters<T>) => ReturnType<T>;

export interface Wait {
wait: number;
}

export type DebouncedWait<T extends AnyAsyncFunction> = Debounced<T> & Wait;

/**
* Creates a debounced function that delays fn after wait milliseconds have elapsed
* since the last time the debounced function was invoked.
*/
export function debounce<T extends AnyFunction>(
fn: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout = undefined;

return (...args: Parameters<T>): void => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
timeout = undefined;
fn(...args);
}, wait);
};
}

export function debouncePromise<T extends AnyAsyncFunction>(
fn: T,
wait: number
): (...args: Parameters<T>) => ReturnType<T> {
): DebouncedWait<T> {
let timeout: NodeJS.Timeout = undefined;
let resolvers: PromiseWithResolvers<Awaited<ReturnType<T>>> | undefined
= undefined;

return (...args: Parameters<T>): ReturnType<T> => {
function wrapper(...args: Parameters<T>): ReturnType<T> {
if (timeout) {
clearTimeout(timeout);
}
Expand All @@ -44,10 +36,12 @@ export function debouncePromise<T extends AnyAsyncFunction>(
} catch (e) {
r.reject(e);
}
}, wait);
}, wrapper.wait);
if (!resolvers) {
resolvers = Promise.withResolvers<Awaited<ReturnType<T>>>();
}
return resolvers.promise as ReturnType<T>;
};
}
wrapper.wait = wait;
return wrapper;
}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
"description": "Mark all diagnostics, even merely informative ones",
"type": "boolean",
"default": true
},
"peggyLanguageServer.debounceMS": {
"description": "Time, in milliseconds, to debounce typing",
"type": "integer",
"default": 200
}
}
},
Expand Down Expand Up @@ -155,7 +160,7 @@
},
"scripts": {
"lint": "eslint .",
"clean": "rimraf ./out ./dist",
"clean": "rimraf ./out ./dist ./coverage",
"vscode:prepublish": "pnpm run clean && pnpm run compile:production",
"compile": "pnpm run check-types && pnpm run lint && node esbuild.js",
"compile:production": "pnpm run check-types && pnpm run lint && node esbuild.js --production",
Expand Down
47 changes: 29 additions & 18 deletions server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { Position, TextDocument } from "vscode-languageserver-textdocument";
import type { SourceNode } from "source-map-generator";
import { WatchMap } from "./watchMap";
import { debounce } from "../common/debounce";
import { debouncePromise } from "../common/debounce";

function getSession(
ast: peggy.ast.Grammar,
Expand All @@ -36,6 +36,7 @@ function getSession(
ast.code = session as unknown as SourceNode;
}

const ID = "peggyLanguageServer";
const AST = new WatchMap<string, any>();
const WORD_RE = /[^\s{}[\]()`~!@#%^&*+\-=|\\;:'",./<>?]+/g;
const PASSES: peggy.compiler.Stages = {
Expand All @@ -60,6 +61,19 @@ const documents = new TextDocuments(TextDocument);
// for open, change and close text document events
documents.listen(connection);

interface PeggySettings {
consoleInfo: boolean;
markInfo: boolean;
debounceMS: number;
}

const defaultSettings: PeggySettings = {
consoleInfo: false,
markInfo: true,
debounceMS: 200,
};
let globalSettings: PeggySettings = defaultSettings;

// After the server has started the client sends an initilize request. The
// server receives in the passed params the rootPath of the workspace plus the
// client capabilites.
Expand All @@ -77,16 +91,9 @@ connection.onInitialize((): InitializeResult => ({
},
}));

interface PeggySettings {
consoleInfo: boolean;
markInfo: boolean;
}

const defaultSettings: PeggySettings = {
consoleInfo: false,
markInfo: true,
};
let globalSettings: PeggySettings = defaultSettings;
connection.onInitialized(async () => {
globalSettings = await connection.workspace.getConfiguration(ID);
});

function peggyLoc_to_vscodeRange(loc: peggy.LocationRange): Range {
if (!loc) {
Expand Down Expand Up @@ -143,7 +150,7 @@ function addProblemDiagnostics(
}
}

const validateTextDocument = debounce((doc: TextDocument): void => {
async function validate(doc: TextDocument): Promise<void> {
AST.delete(doc.uri); // Cancel any pending and start over.

const diagnostics: Diagnostic[] = [];
Expand Down Expand Up @@ -185,13 +192,17 @@ const validateTextDocument = debounce((doc: TextDocument): void => {
}

// Send the computed diagnostics to VS Code.
connection.sendDiagnostics({ uri: doc.uri, diagnostics });
}, 150);
await connection.sendDiagnostics({ uri: doc.uri, diagnostics });
}

connection.onDidChangeConfiguration(change => {
if (change.settings) {
globalSettings = change.settings.peggyLanguageServer as PeggySettings;
}
const validateTextDocument = debouncePromise(
validate,
globalSettings.debounceMS
);

connection.onDidChangeConfiguration(async () => {
globalSettings = await connection.workspace.getConfiguration(ID);
validateTextDocument.wait = globalSettings.debounceMS;

// Revalidate all open text documents
documents.all().forEach(validateTextDocument);
Expand Down
4 changes: 2 additions & 2 deletions test/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as assert from "assert";
import * as assert from "node:assert";
import * as vscode from "vscode";
import { activate, getDocUri, sleep } from "./helper.js";

Expand All @@ -12,7 +12,6 @@ suite("Extension Test Suite", () => {
const docUri = getDocUri("bad.peggy");
const editor = await activate(docUri);
const actualDiagnostics = vscode.languages.getDiagnostics(docUri);
console.error({ actualDiagnostics });
assert.ok(actualDiagnostics);
assert.ok(Array.isArray(actualDiagnostics));
assert.equal(actualDiagnostics.length, 3);
Expand Down Expand Up @@ -52,6 +51,7 @@ suite("Extension Test Suite", () => {
});
await vscode.commands.executeCommand("editor.peggyLive");

// Still needed: check results for correctness.
await sleep(1000);
});
});