Skip to content

Commit c5705dd

Browse files
committed
add filesystem abstraction and remove node API calls
1 parent 802fb83 commit c5705dd

File tree

6 files changed

+152
-32
lines changed

6 files changed

+152
-32
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
5.4.0 / 2023-04-10
2+
================
3+
* Added `FileType`, `FileStat`, and `FileSystemProvider` types to abstract file system access.
4+
* Updated findLinks to recognize `uri-reference` schema values.
5+
16
5.3.1 / 2023-02-24
27
================
38
* Fixing bugs in the sort feature

src/jsonLanguageService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi
6767

6868
const jsonCompletion = new JSONCompletion(jsonSchemaService, params.contributions, promise, params.clientCapabilities);
6969
const jsonHover = new JSONHover(jsonSchemaService, params.contributions, promise);
70-
const jsonLinks = new JSONLinks(jsonSchemaService);
70+
const jsonLinks = new JSONLinks(jsonSchemaService, params.fileSystemProvider);
7171
const jsonDocumentSymbols = new JSONDocumentSymbols(jsonSchemaService);
7272
const jsonValidation = new JSONValidation(jsonSchemaService, promise);
7373

src/jsonLanguageTypes.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,49 @@ export interface Thenable<R> {
252252
then<TResult>(onfulfilled?: (value: R) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
253253
}
254254

255+
export enum FileType {
256+
/**
257+
* The file type is unknown.
258+
*/
259+
Unknown = 0,
260+
/**
261+
* A regular file.
262+
*/
263+
File = 1,
264+
/**
265+
* A directory.
266+
*/
267+
Directory = 2,
268+
/**
269+
* A symbolic link to a file.
270+
*/
271+
SymbolicLink = 64
272+
}
273+
274+
export interface FileStat {
275+
/**
276+
* The type of the file, e.g. is a regular file, a directory, or symbolic link
277+
* to a file.
278+
*/
279+
type: FileType;
280+
/**
281+
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
282+
*/
283+
ctime: number;
284+
/**
285+
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
286+
*/
287+
mtime: number;
288+
/**
289+
* The size in bytes.
290+
*/
291+
size: number;
292+
}
293+
294+
export interface FileSystemProvider {
295+
stat(uri: DocumentUri): Promise<FileStat>;
296+
}
297+
255298
export interface LanguageServiceParams {
256299
/**
257300
* The schema request service is used to fetch schemas from a URI. The provider returns the schema file content, or,
@@ -270,6 +313,10 @@ export interface LanguageServiceParams {
270313
* A promise constructor. If not set, the ES5 Promise will be used.
271314
*/
272315
promiseConstructor?: PromiseConstructor;
316+
/**
317+
* Abstract file system access away from the service.
318+
*/
319+
fileSystemProvider?: FileSystemProvider;
273320
/**
274321
* Describes the LSP capabilities the client supports.
275322
*/

src/services/jsonLinks.ts

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,26 @@
44
*--------------------------------------------------------------------------------------------*/
55

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

1412
export class JSONLinks {
1513
private schemaService: IJSONSchemaService;
14+
private fileSystemProvider: FileSystemProvider | undefined;
1615

17-
constructor(schemaService: IJSONSchemaService) {
16+
constructor(schemaService: IJSONSchemaService, fileSystemProvider?: FileSystemProvider) {
1817
this.schemaService = schemaService;
18+
this.fileSystemProvider = fileSystemProvider;
1919
}
2020

2121
public findLinks(document: TextDocument, doc: JSONDocument): Thenable<DocumentLink[]> {
22-
return findLinks(document, doc, this.schemaService);
22+
return findLinks(document, doc, this.schemaService, this.fileSystemProvider);
2323
}
2424
}
2525

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

2929
const refLinks: DocumentLink[] = [];
@@ -39,32 +39,43 @@ export function findLinks(document: TextDocument, doc: JSONDocument, schemaServi
3939
});
4040
}
4141
}
42-
if (node.type === "property" && node.valueNode?.type === 'string' && schemaService) {
42+
if (node.type === "property" && node.valueNode?.type === 'string' && schemaService && fileSystemProvider) {
4343
const pathNode = node.valueNode;
4444
const promise = schemaService.getSchemaForResource(document.uri, doc).then((schema) => {
4545
const pathLinks: DocumentLink[] = [];
4646
if (!schema) {
4747
return pathLinks;
4848
}
49-
doc.getMatchingSchemas(schema.schema, pathNode.offset).forEach((s) => {
49+
50+
const matchingSchemas = doc.getMatchingSchemas(schema.schema, pathNode.offset);
51+
52+
let resolvedRef = '';
53+
for (const s of matchingSchemas) {
5054
if (s.node !== pathNode || s.inverted || !s.schema) {
51-
return; // Not an _exact_ schema match.
55+
continue; // Not an _exact_ schema match.
5256
}
5357
if (s.schema.format !== 'uri-reference') {
54-
return; // Not a uri-ref.
58+
continue; // Not a uri-ref.
5559
}
5660
const pathURI = resolveURIRef(pathNode.value, document);
57-
if (!pathURI) {
58-
return; // Unable to resolve ref.
61+
if (pathURI.scheme === 'file') {
62+
resolvedRef = pathURI.toString();
5963
}
60-
if (fileExistsSync(pathURI.fsPath)) {
61-
pathLinks.push({
62-
target: pathURI.toString(),
63-
range: createRange(document, pathNode)
64-
});
65-
}
66-
});
67-
return pathLinks;
64+
}
65+
66+
if (resolvedRef) {
67+
return fileSystemProvider.stat(resolvedRef).then((fs) => {
68+
if (fileExists(fs)) {
69+
pathLinks.push({
70+
target: resolvedRef,
71+
range: createRange(document, pathNode)
72+
});
73+
}
74+
return pathLinks;
75+
});
76+
} else {
77+
return pathLinks;
78+
}
6879
});
6980
promises.push(promise);
7081
}
@@ -77,6 +88,13 @@ export function findLinks(document: TextDocument, doc: JSONDocument, schemaServi
7788
});
7889
}
7990

91+
function fileExists(stat: FileStat): boolean {
92+
if (stat.type === FileType.Unknown && stat.size === -1) {
93+
return false;
94+
}
95+
return true;
96+
}
97+
8098
function createRange(document: TextDocument, node: ASTNode): Range {
8199
return Range.create(document.positionAt(node.offset + 1), document.positionAt(node.offset + node.length - 1));
82100
}
@@ -133,12 +151,10 @@ function unescape(str: string): string {
133151
return str.replace(/~1/g, '/').replace(/~0/g, '~');
134152
}
135153

136-
function resolveURIRef(ref: string, document: TextDocument): URI | null {
154+
function resolveURIRef(ref: string, document: TextDocument): URI {
137155
if (ref.indexOf('://') > 0) {
138156
// Already a fully qualified URI.
139-
// The language service should already create a document link
140-
// for these, so no need to created a duplicate.
141-
return null;
157+
return URI.parse(ref);
142158
}
143159

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

149165
// Resolve ref relative to the document.
150166
const docURI = URI.parse(document.uri);
151-
const docDir = path.dirname(docURI.path);
152-
const refPath = path.join(docDir, ref);
153-
return docURI.with({
154-
path: refPath
155-
});
167+
return Utils.joinPath(Utils.dirname(docURI), ref);
156168
}

src/test/links.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Range,
1414
TextDocument,
1515
} from '../jsonLanguageService';
16+
import { getFsProvider } from './testUtil/fsProvider';
1617
import * as path from 'path';
1718
import { URI } from 'vscode-uri';
1819

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

41-
const ls = getLanguageService({ clientCapabilities: ClientCapabilities.LATEST });
42+
const ls = getLanguageService({
43+
clientCapabilities: ClientCapabilities.LATEST,
44+
fileSystemProvider: getFsProvider(),
45+
});
4246
ls.configure({ schemas: [{ fileMatch: ["*.json"], uri: schemaUri, schema }] });
4347
const jsonDoc = ls.parseJSONDocument(document);
4448

src/test/testUtil/fsProvider.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { FileSystemProvider, FileType } from "../../jsonLanguageTypes";
7+
import { URI } from 'vscode-uri';
8+
import { stat as fsStat } from 'fs';
9+
10+
export function getFsProvider(): FileSystemProvider {
11+
return {
12+
stat(documentUriString: string) {
13+
return new Promise((c, e) => {
14+
const documentUri = URI.parse(documentUriString);
15+
if (documentUri.scheme !== 'file') {
16+
e(new Error('Protocol not supported: ' + documentUri.scheme));
17+
return;
18+
}
19+
fsStat(documentUri.fsPath, (err, stats) => {
20+
if (err) {
21+
if (err.code === 'ENOENT') {
22+
return c({
23+
type: FileType.Unknown,
24+
ctime: -1,
25+
mtime: -1,
26+
size: -1
27+
});
28+
} else {
29+
return e(err);
30+
}
31+
}
32+
33+
let type = FileType.Unknown;
34+
if (stats.isFile()) {
35+
type = FileType.File;
36+
} else if (stats.isDirectory()) {
37+
type = FileType.Directory;
38+
} else if (stats.isSymbolicLink()) {
39+
type = FileType.SymbolicLink;
40+
}
41+
42+
c({
43+
type,
44+
ctime: stats.ctime.getTime(),
45+
mtime: stats.mtime.getTime(),
46+
size: stats.size
47+
});
48+
});
49+
});
50+
},
51+
};
52+
}

0 commit comments

Comments
 (0)