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
8 changes: 4 additions & 4 deletions language-server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 0 additions & 12 deletions language-server/src/build-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { Diagnostics } from "./features/Diagnostics.ts";
import { SyntaxValidation } from "./features/SyntaxValidation.ts";
import { SchemaValidation } from "./features/SchemaValidation.ts";
import { Formatting } from "./features/Formatting.ts";
import { addMediaTypePlugin, removeUriSchemePlugin } from "@hyperjump/browser";
import { buildSchemaDocument } from "@hyperjump/json-schema/experimental";
import { Hover } from "./features/Hover.ts";
import { Completion } from "./features/Completion.ts";
import { FoldingRanges } from "./features/FoldingRanges.ts";
Expand All @@ -23,16 +21,6 @@ import type { Connection } from "vscode-languageserver";
export type LanguageServerSettings = {
};

addMediaTypePlugin("application/json", {
parse: async (response) => {
return buildSchemaDocument(await response.json(), response.url);
},
fileMatcher: async (path) => path.endsWith(".json")
});

removeUriSchemePlugin("http");
removeUriSchemePlugin("https");

export const buildServer = (connection: Connection): Server => {
const server = new Server(connection);

Expand Down
4 changes: 2 additions & 2 deletions language-server/src/features/Completion.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { CompletionItemKind, ServerCapabilities } from "vscode-languageserver";
import { CompletionItemKind } from "vscode-languageserver";
import { JsonDocuments } from "../services/JsonDocuments.ts";

import type { Server } from "../services/Server.ts";
import type { CompletionItem } from "vscode-languageserver";
import type { CompletionItem, ServerCapabilities } from "vscode-languageserver";

export class Completion {
constructor(server: Server, jsonDocuments: JsonDocuments) {
Expand Down
5 changes: 0 additions & 5 deletions language-server/src/features/Hover.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { HoverRequest, PublishDiagnosticsNotification } from "vscode-languageserver";
import { TestClient } from "../test/TestClient.ts";
import { unregisterSchema } from "@hyperjump/json-schema";

describe("Hover", () => {
let client: TestClient;
Expand All @@ -16,10 +15,6 @@ describe("Hover", () => {
await client.stop();
});

afterEach(() => {
unregisterSchema(fixtureSchemaUri);
});

test("should return title and description on hover over a property value", async () => {
const diagnostics: Promise<void> = new Promise((resolve) => {
client.onNotification(PublishDiagnosticsNotification.type, () => {
Expand Down
14 changes: 14 additions & 0 deletions language-server/src/protocol/hyperjump-findFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { CM, MessageDirection, ProtocolRequestType } from "vscode-languageserver";

export type FindFilesParams = {
include: string;
exclude?: string;
maxResults?: number;
};

export const FindFilesRequest = {
method: "hyperjump/findFiles" as const,
messageDirection: MessageDirection.serverToClient,
type: new ProtocolRequestType<FindFilesParams, string[], never, void, void>("hyperjump/findFiles"),
capabilities: CM.create("hyperjump.findFiles", undefined)
};
12 changes: 12 additions & 0 deletions language-server/src/protocol/hyperjump-readFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CM, MessageDirection, ProtocolRequestType } from "vscode-languageserver";

export type ReadFileParams = {
uri: string;
};

export const ReadFileRequest = {
method: "hyperjump/readFile" as const,
messageDirection: MessageDirection.serverToClient,
type: new ProtocolRequestType<ReadFileParams, string, never, void, void>("hyperjump/readFile"),
capabilities: CM.create("hyperjump.readFile", undefined)
};
86 changes: 43 additions & 43 deletions language-server/src/services/SchemaStore.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { compile, getSchema, getKeywordName } from "@hyperjump/json-schema/experimental";
import { compile, getSchema, getKeywordName, buildSchemaDocument } from "@hyperjump/json-schema/experimental";
import { registerSchema, unregisterSchema } from "@hyperjump/json-schema";
import { evaluateCompiledSchema } from "@hyperjump/json-schema-errors";
import { addUriSchemePlugin, httpSchemePlugin } from "@hyperjump/browser";
import { normalizeIri, resolveIri, toAbsoluteIri } from "@hyperjump/uri";
import { addMediaTypePlugin, addUriSchemePlugin, getFileMediaType, httpSchemePlugin } from "@hyperjump/browser";
import { normalizeIri, parseIri, resolveIri, toAbsoluteIri, toRelativeIri } from "@hyperjump/uri";
import * as jsonc from "jsonc-parser";
import * as Pact from "@hyperjump/pact";
import ignore from "ignore";
Expand Down Expand Up @@ -54,7 +51,11 @@ export class SchemaStore {

this.scanCompleted = new Promise((resolve) => {
server.onInitialized(async () => {
await this.scanWorkspace();
this.server.console.log("Scanning workspace for self-identifying schemas...");
for (const fileUri of await this.workspace.findFiles("**/*.{json,jsonc}")) {
Comment thread
jdesrosiers marked this conversation as resolved.
await this.processWorkspaceSchemaFile(fileUri);
}
this.server.console.log("Scanning completed");
resolve();
});
});
Expand All @@ -67,6 +68,13 @@ export class SchemaStore {
);
});

addMediaTypePlugin("application/json", {
parse: async (response) => {
return buildSchemaDocument(await response.json(), response.url);
},
fileMatcher: async (path) => path.endsWith(".json")
});

const uriSchemePlugin: UriSchemePlugin = {
async retrieve(uri: string) {
if (!(await schemaAllowList).has(uri) && !uri.startsWith("https://json.schemastore.org")) {
Expand All @@ -76,10 +84,33 @@ export class SchemaStore {
return httpSchemePlugin.retrieve(uri);
}
};

addUriSchemePlugin("http", uriSchemePlugin);
addUriSchemePlugin("https", uriSchemePlugin);

addUriSchemePlugin("file", {
async retrieve(uri, baseUri) {
if (baseUri) {
const { scheme } = parseIri(baseUri);

if (scheme !== "file") {
throw Error(`Accessing a file (${uri}) from a non-filesystem context (${baseUri}) is not allowed`);
}
}

let responseUri = toAbsoluteIri(uri);

const contentType = await getFileMediaType(responseUri);
const file = await workspace.readFile(uri);
const stream = new Blob([file]).stream();
const response = new Response(stream, {
headers: { "Content-Type": contentType }
});
Object.defineProperty(response, "url", { value: responseUri });

return response;
}
});

workspace.onDidChangeWatchedFiles(async (params) => {
for (const change of params.changes) {
const changedSchemaUri = normalizeIri(change.uri);
Expand All @@ -92,22 +123,18 @@ export class SchemaStore {
}

async getSchemaUri(fileUri: string) {
const filePath = fileURLToPath(fileUri);

for (const schema of await this.catalog) {
const { fileMatch, url } = schema;
for (const { fileMatch, url } of await this.catalog) {
if (!fileMatch) {
continue;
}

const ig = ignore().add(fileMatch);
for (const workspaceUri of this.workspace.workspaceFolders) {
const workspacePath = fileURLToPath(workspaceUri);
if (!filePath.startsWith(workspacePath)) {
if (!fileUri.startsWith(workspaceUri)) {
continue;
}

const relativePath = path.relative(workspacePath, filePath);
const relativePath = toRelativeIri(workspaceUri, fileUri);
if (ig.ignores(relativePath)) {
return url;
}
Expand Down Expand Up @@ -172,36 +199,9 @@ export class SchemaStore {
return dependentSchemas;
}

private async scanWorkspace() {
this.server.console.log("Scanning workspace for self-identifying schemas...");
for (const folderUri of this.workspace.workspaceFolders) {
const dirPath = fileURLToPath(folderUri);

const ig = ignore();
try {
const gitignorePath = path.join(dirPath, ".gitignore");
const gitignoreContent = await fs.readFile(gitignorePath, "utf-8");
ig.add(gitignoreContent);
} catch {
// Ignore if .gitignore does not exist
}

for await (const entry of fs.glob("**/*.{json,jsonc}", { cwd: dirPath, exclude: [".git/"] })) {
if (ig.ignores(entry)) {
continue;
}
const fullPath = path.join(dirPath, entry);
const fileUri = pathToFileURL(fullPath).toString();
await this.processWorkspaceSchemaFile(fileUri);
}
}
this.server.console.log("Scanning completed");
}

private async processWorkspaceSchemaFile(fileUri: string) {
const filePath = fileURLToPath(fileUri);
try {
const text = await fs.readFile(filePath, "utf-8");
const text = await this.workspace.readFile(fileUri);
const schema = jsonc.parse(text);

if (typeof schema?.["$schema"] === "string") {
Expand Down
21 changes: 17 additions & 4 deletions language-server/src/services/Workspace.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import {
DidChangeWatchedFilesNotification,
import { DidChangeWatchedFilesNotification, Disposable } from "vscode-languageserver";
import { Server } from "./Server.ts";
import { ReadFileRequest } from "../protocol/hyperjump-readFile.ts";
import { FindFilesRequest } from "../protocol/hyperjump-findFiles.ts";

import type {
DidChangeWatchedFilesParams,
Disposable,
NotificationHandler,
ServerCapabilities
} from "vscode-languageserver";
import { Server } from "./Server.ts";

export class Workspace {
private server: Server;
private _workspaceFolders: Set<string> = new Set();
private didChangeWatchedFilesHandlers: Set<NotificationHandler<DidChangeWatchedFilesParams>>;

constructor(server: Server) {
this.server = server;

let hasWorkspaceWatchCapability = false;
let hasWorkspaceFolderCapability = false;

Expand Down Expand Up @@ -82,4 +87,12 @@ export class Workspace {
}
};
}

async readFile(uri: string) {
return await this.server.sendRequest(ReadFileRequest.type, { uri });
}

async findFiles(include: string, exclude?: string, maxResults?: number) {
return await this.server.sendRequest(FindFilesRequest.type, { include, exclude, maxResults });
}
}
44 changes: 40 additions & 4 deletions language-server/src/test/TestClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { access, glob, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { Duplex } from "node:stream";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
Expand All @@ -18,17 +18,22 @@ import {
ShutdownRequest
} from "vscode-languageserver";
import { createConnection } from "vscode-languageserver/node";
import { resolveIri } from "@hyperjump/uri";
import { normalizeIri, resolveIri } from "@hyperjump/uri";
import { merge } from "merge-anything";
import { MockAgent, setGlobalDispatcher } from "undici";
import { buildServer, LanguageServerSettings } from "../build-server.js";
import ignore from "ignore";
import * as Pact from "@hyperjump/pact";
import { buildServer } from "../build-server.js";
import { FindFilesRequest } from "../protocol/hyperjump-findFiles.ts";
import { ReadFileRequest } from "../protocol/hyperjump-readFile.ts";

import type {
Connection,
DidChangeConfigurationRegistrationOptions,
InitializeParams,
ServerCapabilities
} from "vscode-languageserver";
import type { LanguageServerSettings } from "../build-server.js";

export class TestClient {
private client: Connection;
Expand All @@ -40,6 +45,7 @@ export class TestClient {
private openDocuments: Set<string>;
private workspaceFolder: Promise<string>;
private ready: Promise<void>;
private gitignore: Promise<string>;

onRequest: Connection["onRequest"];
sendRequest: Connection["sendRequest"];
Expand All @@ -54,7 +60,15 @@ export class TestClient {
this.watchEnabled = false;
this.openDocuments = new Set();
this.workspaceFolder = mkdtemp(join(tmpdir(), "test-workspace-"))
.then((path) => pathToFileURL(path) + "/");
.then((path) => normalizeIri(pathToFileURL(path) + "/"));
this.gitignore = this.workspaceFolder.then(async (rootPath) => {
const gitignorePath = join(rootPath, ".gitignore");
try {
return await readFile(gitignorePath, "utf8");
} catch {
return "";
}
});

this.mockAgent = new MockAgent();
this.mockAgent.disableNetConnect();
Expand Down Expand Up @@ -102,6 +116,28 @@ export class TestClient {
});
});

this.client.onRequest(ReadFileRequest.type, async (params) => {
const path = fileURLToPath(params.uri);
return await readFile(path, "utf8");
});

this.client.onRequest(FindFilesRequest.type, async (params) => {
const ig = ignore()
.add(await this.gitignore)
.add(params.exclude ?? "");

const workspacePath = fileURLToPath(await this.workspaceFolder);

return await Pact.pipe(
glob(params.include, { cwd: workspacePath }),
Pact.asyncFilter((file) => !ig.ignores(file)),
Pact.asyncMap((relativePath: string) => join(workspacePath, relativePath)),
Pact.asyncMap((fullPath: string) => pathToFileURL(fullPath).toString()),
Pact.asyncTake(params.maxResults ?? Number.MAX_SAFE_INTEGER),
Pact.asyncCollectArray
);
});

this.client.listen();
}

Expand Down
2 changes: 2 additions & 0 deletions language-server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"checkJs": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"types": ["node"]
}
Expand Down
Loading
Loading