Skip to content

Commit

Permalink
add filesystem abstraction and remove node API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
twelvelabs committed Feb 11, 2024
1 parent 802fb83 commit c5705dd
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 32 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
5.4.0 / 2023-04-10
================
* Added `FileType`, `FileStat`, and `FileSystemProvider` types to abstract file system access.
* Updated findLinks to recognize `uri-reference` schema values.

5.3.1 / 2023-02-24
================
* Fixing bugs in the sort feature
Expand Down
2 changes: 1 addition & 1 deletion src/jsonLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi

const jsonCompletion = new JSONCompletion(jsonSchemaService, params.contributions, promise, params.clientCapabilities);
const jsonHover = new JSONHover(jsonSchemaService, params.contributions, promise);
const jsonLinks = new JSONLinks(jsonSchemaService);
const jsonLinks = new JSONLinks(jsonSchemaService, params.fileSystemProvider);
const jsonDocumentSymbols = new JSONDocumentSymbols(jsonSchemaService);
const jsonValidation = new JSONValidation(jsonSchemaService, promise);

Expand Down
47 changes: 47 additions & 0 deletions src/jsonLanguageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,49 @@ export interface Thenable<R> {
then<TResult>(onfulfilled?: (value: R) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
}

export enum FileType {
/**
* The file type is unknown.
*/
Unknown = 0,
/**
* A regular file.
*/
File = 1,
/**
* A directory.
*/
Directory = 2,
/**
* A symbolic link to a file.
*/
SymbolicLink = 64
}

export interface FileStat {
/**
* The type of the file, e.g. is a regular file, a directory, or symbolic link
* to a file.
*/
type: FileType;
/**
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
ctime: number;
/**
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
mtime: number;
/**
* The size in bytes.
*/
size: number;
}

export interface FileSystemProvider {
stat(uri: DocumentUri): Promise<FileStat>;
}

export interface LanguageServiceParams {
/**
* The schema request service is used to fetch schemas from a URI. The provider returns the schema file content, or,
Expand All @@ -270,6 +313,10 @@ export interface LanguageServiceParams {
* A promise constructor. If not set, the ES5 Promise will be used.
*/
promiseConstructor?: PromiseConstructor;
/**
* Abstract file system access away from the service.
*/
fileSystemProvider?: FileSystemProvider;
/**
* Describes the LSP capabilities the client supports.
*/
Expand Down
72 changes: 42 additions & 30 deletions src/services/jsonLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@
*--------------------------------------------------------------------------------------------*/

import { DocumentLink } from 'vscode-languageserver-types';
import { TextDocument, ASTNode, PropertyASTNode, Range, Thenable } from '../jsonLanguageTypes';
import { TextDocument, ASTNode, PropertyASTNode, Range, Thenable, FileSystemProvider, FileType, FileStat } from '../jsonLanguageTypes';
import { JSONDocument } from '../parser/jsonParser';
import { IJSONSchemaService } from './jsonSchemaService';
import { URI } from 'vscode-uri';
import { existsSync as fileExistsSync } from 'fs';
import * as path from 'path';
import { URI, Utils } from 'vscode-uri';

export class JSONLinks {
private schemaService: IJSONSchemaService;
private fileSystemProvider: FileSystemProvider | undefined;

constructor(schemaService: IJSONSchemaService) {
constructor(schemaService: IJSONSchemaService, fileSystemProvider?: FileSystemProvider) {
this.schemaService = schemaService;
this.fileSystemProvider = fileSystemProvider;
}

public findLinks(document: TextDocument, doc: JSONDocument): Thenable<DocumentLink[]> {
return findLinks(document, doc, this.schemaService);
return findLinks(document, doc, this.schemaService, this.fileSystemProvider);
}
}

export function findLinks(document: TextDocument, doc: JSONDocument, schemaService?: IJSONSchemaService): Thenable<DocumentLink[]> {
export function findLinks(document: TextDocument, doc: JSONDocument, schemaService?: IJSONSchemaService, fileSystemProvider?: FileSystemProvider): Thenable<DocumentLink[]> {
const promises: Thenable<DocumentLink[]>[] = [];

const refLinks: DocumentLink[] = [];
Expand All @@ -39,32 +39,43 @@ export function findLinks(document: TextDocument, doc: JSONDocument, schemaServi
});
}
}
if (node.type === "property" && node.valueNode?.type === 'string' && schemaService) {
if (node.type === "property" && node.valueNode?.type === 'string' && schemaService && fileSystemProvider) {
const pathNode = node.valueNode;
const promise = schemaService.getSchemaForResource(document.uri, doc).then((schema) => {
const pathLinks: DocumentLink[] = [];
if (!schema) {
return pathLinks;
}
doc.getMatchingSchemas(schema.schema, pathNode.offset).forEach((s) => {

const matchingSchemas = doc.getMatchingSchemas(schema.schema, pathNode.offset);

let resolvedRef = '';
for (const s of matchingSchemas) {
if (s.node !== pathNode || s.inverted || !s.schema) {
return; // Not an _exact_ schema match.
continue; // Not an _exact_ schema match.
}
if (s.schema.format !== 'uri-reference') {
return; // Not a uri-ref.
continue; // Not a uri-ref.
}
const pathURI = resolveURIRef(pathNode.value, document);
if (!pathURI) {
return; // Unable to resolve ref.
if (pathURI.scheme === 'file') {
resolvedRef = pathURI.toString();
}
if (fileExistsSync(pathURI.fsPath)) {
pathLinks.push({
target: pathURI.toString(),
range: createRange(document, pathNode)
});
}
});
return pathLinks;
}

if (resolvedRef) {
return fileSystemProvider.stat(resolvedRef).then((fs) => {
if (fileExists(fs)) {
pathLinks.push({
target: resolvedRef,
range: createRange(document, pathNode)
});
}
return pathLinks;
});
} else {
return pathLinks;
}
});
promises.push(promise);
}
Expand All @@ -77,6 +88,13 @@ export function findLinks(document: TextDocument, doc: JSONDocument, schemaServi
});
}

function fileExists(stat: FileStat): boolean {
if (stat.type === FileType.Unknown && stat.size === -1) {
return false;
}
return true;
}

function createRange(document: TextDocument, node: ASTNode): Range {
return Range.create(document.positionAt(node.offset + 1), document.positionAt(node.offset + node.length - 1));
}
Expand Down Expand Up @@ -133,12 +151,10 @@ function unescape(str: string): string {
return str.replace(/~1/g, '/').replace(/~0/g, '~');
}

function resolveURIRef(ref: string, document: TextDocument): URI | null {
function resolveURIRef(ref: string, document: TextDocument): URI {
if (ref.indexOf('://') > 0) {
// Already a fully qualified URI.
// The language service should already create a document link
// for these, so no need to created a duplicate.
return null;
return URI.parse(ref);
}

if (ref.startsWith('/')) {
Expand All @@ -148,9 +164,5 @@ function resolveURIRef(ref: string, document: TextDocument): URI | null {

// Resolve ref relative to the document.
const docURI = URI.parse(document.uri);
const docDir = path.dirname(docURI.path);
const refPath = path.join(docDir, ref);
return docURI.with({
path: refPath
});
return Utils.joinPath(Utils.dirname(docURI), ref);
}
6 changes: 5 additions & 1 deletion src/test/links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Range,
TextDocument,
} from '../jsonLanguageService';
import { getFsProvider } from './testUtil/fsProvider';
import * as path from 'path';
import { URI } from 'vscode-uri';

Expand All @@ -38,7 +39,10 @@ suite('JSON Find Links', () => {
function testFindLinksWithSchema(document: TextDocument, schema: JSONSchema): PromiseLike<DocumentLink[]> {
const schemaUri = "http://myschemastore/test1";

const ls = getLanguageService({ clientCapabilities: ClientCapabilities.LATEST });
const ls = getLanguageService({
clientCapabilities: ClientCapabilities.LATEST,
fileSystemProvider: getFsProvider(),
});
ls.configure({ schemas: [{ fileMatch: ["*.json"], uri: schemaUri, schema }] });
const jsonDoc = ls.parseJSONDocument(document);

Expand Down
52 changes: 52 additions & 0 deletions src/test/testUtil/fsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { FileSystemProvider, FileType } from "../../jsonLanguageTypes";
import { URI } from 'vscode-uri';
import { stat as fsStat } from 'fs';

export function getFsProvider(): FileSystemProvider {
return {
stat(documentUriString: string) {
return new Promise((c, e) => {
const documentUri = URI.parse(documentUriString);
if (documentUri.scheme !== 'file') {
e(new Error('Protocol not supported: ' + documentUri.scheme));
return;
}
fsStat(documentUri.fsPath, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return c({
type: FileType.Unknown,
ctime: -1,
mtime: -1,
size: -1
});
} else {
return e(err);
}
}

let type = FileType.Unknown;
if (stats.isFile()) {
type = FileType.File;
} else if (stats.isDirectory()) {
type = FileType.Directory;
} else if (stats.isSymbolicLink()) {
type = FileType.SymbolicLink;
}

c({
type,
ctime: stats.ctime.getTime(),
mtime: stats.mtime.getTime(),
size: stats.size
});
});
});
},
};
}

0 comments on commit c5705dd

Please sign in to comment.