From bcf97e5b7c12479fbc961b0f549863913bb1e6b5 Mon Sep 17 00:00:00 2001 From: Skip Baney Date: Sat, 13 Jan 2024 19:44:21 -0600 Subject: [PATCH 1/6] create links for values of format: uri-reference --- src/jsonLanguageService.ts | 5 ++- src/services/jsonLinks.ts | 57 +++++++++++++++++++++++++--- src/test/fixtures/uri-reference.txt | 0 src/test/links.test.ts | 58 ++++++++++++++++++++++++++++- 4 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 src/test/fixtures/uri-reference.txt diff --git a/src/jsonLanguageService.ts b/src/jsonLanguageService.ts index 1e2e54fa..9124ba55 100644 --- a/src/jsonLanguageService.ts +++ b/src/jsonLanguageService.ts @@ -26,7 +26,7 @@ import { Position, CompletionItem, CompletionList, Hover, Range, SymbolInformation, Diagnostic, TextEdit, FormattingOptions, DocumentSymbol, DefinitionLink, MatchingSchema, JSONLanguageStatus, SortOptions } from './jsonLanguageTypes'; -import { findLinks } from './services/jsonLinks'; +import { JSONLinks } from './services/jsonLinks'; import { DocumentLink } from 'vscode-languageserver-types'; export type JSONDocument = { @@ -67,6 +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 jsonDocumentSymbols = new JSONDocumentSymbols(jsonSchemaService); const jsonValidation = new JSONValidation(jsonSchemaService, promise); @@ -92,7 +93,7 @@ export function getLanguageService(params: LanguageServiceParams): LanguageServi getFoldingRanges, getSelectionRanges, findDefinition: () => Promise.resolve([]), - findLinks, + findLinks: jsonLinks.findLinks.bind(jsonLinks), format: (document: TextDocument, range: Range, options: FormattingOptions) => format(document, options, range), sort: (document: TextDocument, options: FormattingOptions) => sort(document, options) }; diff --git a/src/services/jsonLinks.ts b/src/services/jsonLinks.ts index 3a73e4ff..6c485642 100644 --- a/src/services/jsonLinks.ts +++ b/src/services/jsonLinks.ts @@ -6,24 +6,71 @@ import { DocumentLink } from 'vscode-languageserver-types'; import { TextDocument, ASTNode, PropertyASTNode, Range, Thenable } from '../jsonLanguageTypes'; import { JSONDocument } from '../parser/jsonParser'; +import { IJSONSchemaService } from './jsonSchemaService'; +import { URI } from 'vscode-uri'; +import { existsSync as fileExistsSync } from 'fs'; +import { resolve as resolvePath } from 'path'; -export function findLinks(document: TextDocument, doc: JSONDocument): Thenable { - const links: DocumentLink[] = []; +export class JSONLinks { + private schemaService: IJSONSchemaService; + + constructor(schemaService: IJSONSchemaService) { + this.schemaService = schemaService; + } + + public findLinks(document: TextDocument, doc: JSONDocument): Thenable { + return findLinks(document, doc, this.schemaService); + } +} + +export function findLinks(document: TextDocument, doc: JSONDocument, schemaService?: IJSONSchemaService): Thenable { + const promises: Thenable[] = []; + + const refLinks: DocumentLink[] = []; doc.visit(node => { - if (node.type === "property" && node.keyNode.value === "$ref" && node.valueNode?.type === 'string') { + if (node.type === "property" && node.valueNode?.type === 'string' && node.keyNode.value === "$ref") { const path = node.valueNode.value; const targetNode = findTargetNode(doc, path); if (targetNode) { const targetPos = document.positionAt(targetNode.offset); - links.push({ + refLinks.push({ target: `${document.uri}#${targetPos.line + 1},${targetPos.character + 1}`, range: createRange(document, node.valueNode) }); } } + if (node.type === "property" && node.valueNode?.type === 'string' && schemaService) { + const pathNode = node.valueNode; + const pathLinks: DocumentLink[] = []; + const promise = schemaService.getSchemaForResource(document.uri, doc).then((schema) => { + if (schema) { + const matchingSchemas = doc.getMatchingSchemas(schema.schema, pathNode.offset); + matchingSchemas.forEach((s) => { + if (s.node === pathNode && !s.inverted && s.schema) { + if (s.schema.format === 'uri-reference') { + const path = resolvePath(pathNode.value); + const uri = URI.file(path); + if (fileExistsSync(path)) { + pathLinks.push({ + target: uri.toString(), + range: createRange(document, pathNode) + }); + } + } + } + }); + } + return pathLinks; + }); + promises.push(promise); + } return true; }); - return Promise.resolve(links); + + promises.push(Promise.resolve(refLinks)); + return Promise.all(promises).then(values => { + return values.flat(); + }); } function createRange(document: TextDocument, node: ASTNode): Range { diff --git a/src/test/fixtures/uri-reference.txt b/src/test/fixtures/uri-reference.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/test/links.test.ts b/src/test/links.test.ts index 4594703a..b6e9cf14 100644 --- a/src/test/links.test.ts +++ b/src/test/links.test.ts @@ -5,7 +5,15 @@ import * as assert from 'assert'; -import { getLanguageService, Range, TextDocument, ClientCapabilities } from '../jsonLanguageService'; +import { + ClientCapabilities, + DocumentLink, + getLanguageService, + JSONSchema, + Range, + TextDocument, +} from '../jsonLanguageService'; +import { resolve as resolvePath } from 'path'; suite('JSON Find Links', () => { const testFindLinksFor = function (value: string, expected: {offset: number, length: number, target: number} | null): PromiseLike { @@ -26,6 +34,20 @@ suite('JSON Find Links', () => { }); }; + let requestService = function (uri: string): Promise { + return Promise.reject('Resource not found'); + }; + + function testFindLinksWithSchema(document: TextDocument, schema: JSONSchema): PromiseLike { + const schemaUri = "http://myschemastore/test1"; + + const ls = getLanguageService({ clientCapabilities: ClientCapabilities.LATEST }); + ls.configure({ schemas: [{ fileMatch: ["*.json"], uri: schemaUri, schema }] }); + const jsonDoc = ls.parseJSONDocument(document); + + return ls.findLinks(document, jsonDoc); + } + test('FindDefinition invalid ref', async function () { await testFindLinksFor('{}', null); await testFindLinksFor('{"name": "John"}', null); @@ -52,4 +74,38 @@ suite('JSON Find Links', () => { await testFindLinksFor(doc('#/ '), {target: 81, offset: 102, length: 3}); await testFindLinksFor(doc('#/m~0n'), {target: 90, offset: 102, length: 6}); }); + + test('URI reference link', async function () { + const relativePath = './src/test/fixtures/uri-reference.txt'; + const content = `{"stringProp": "string-value", "uriProp": "${relativePath}", "uriPropNotFound": "./does/not/exist.txt"}`; + const document = TextDocument.create('test://test.json', 'json', 0, content); + const schema: JSONSchema = { + type: 'object', + properties: { + 'stringProp': { + type: 'string', + }, + 'uriProp': { + type: 'string', + format: 'uri-reference' + }, + 'uriPropNotFound': { + type: 'string', + format: 'uri-reference' + } + } + }; + await testFindLinksWithSchema(document, schema).then((links) => { + assert.notDeepEqual(links, []); + + const absPath = resolvePath(relativePath); + assert.equal(links[0].target, `file://${absPath}`); + + const startOffset = content.indexOf(relativePath); + const endOffset = startOffset + relativePath.length; + const range = Range.create(document.positionAt(startOffset), document.positionAt(endOffset)); + assert.deepEqual(links[0].range, range); + }); + }); + }); From 0cf04341a2052a8b9e8abf3ffb1e4874afe46541 Mon Sep 17 00:00:00 2001 From: Skip Baney Date: Sat, 13 Jan 2024 20:22:39 -0600 Subject: [PATCH 2/6] remove unused func --- src/test/links.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/links.test.ts b/src/test/links.test.ts index b6e9cf14..e99b9a17 100644 --- a/src/test/links.test.ts +++ b/src/test/links.test.ts @@ -34,10 +34,6 @@ suite('JSON Find Links', () => { }); }; - let requestService = function (uri: string): Promise { - return Promise.reject('Resource not found'); - }; - function testFindLinksWithSchema(document: TextDocument, schema: JSONSchema): PromiseLike { const schemaUri = "http://myschemastore/test1"; From 3ebbdc1040daf2827c4cdd6f1efdbbe156f3f180 Mon Sep 17 00:00:00 2001 From: Skip Baney Date: Sun, 14 Jan 2024 22:22:56 -0600 Subject: [PATCH 3/6] resolve uri-ref relative to document --- src/services/jsonLinks.ts | 39 +++++++++++++++++++++++++++++++-------- src/test/links.test.ts | 20 ++++++++++++-------- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/services/jsonLinks.ts b/src/services/jsonLinks.ts index 6c485642..7c40292f 100644 --- a/src/services/jsonLinks.ts +++ b/src/services/jsonLinks.ts @@ -9,7 +9,7 @@ import { JSONDocument } from '../parser/jsonParser'; import { IJSONSchemaService } from './jsonSchemaService'; import { URI } from 'vscode-uri'; import { existsSync as fileExistsSync } from 'fs'; -import { resolve as resolvePath } from 'path'; +import * as path from 'path'; export class JSONLinks { private schemaService: IJSONSchemaService; @@ -48,13 +48,14 @@ export function findLinks(document: TextDocument, doc: JSONDocument, schemaServi matchingSchemas.forEach((s) => { if (s.node === pathNode && !s.inverted && s.schema) { if (s.schema.format === 'uri-reference') { - const path = resolvePath(pathNode.value); - const uri = URI.file(path); - if (fileExistsSync(path)) { - pathLinks.push({ - target: uri.toString(), - range: createRange(document, pathNode) - }); + const pathURI = resolveURIRef(pathNode.value, document); + if (pathURI) { + if (fileExistsSync(pathURI.fsPath)) { + pathLinks.push({ + target: pathURI.toString(), + range: createRange(document, pathNode) + }); + } } } } @@ -128,3 +129,25 @@ function parseJSONPointer(path: string): string[] | null { function unescape(str: string): string { return str.replace(/~1/g, '/').replace(/~0/g, '~'); } + +function resolveURIRef(ref: string, document: TextDocument): URI | null { + 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; + } + + if (ref.startsWith('/')) { + // Already an absolute path, no need to resolve. + return URI.file(ref); + } + + // 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 + }); +} diff --git a/src/test/links.test.ts b/src/test/links.test.ts index e99b9a17..87345537 100644 --- a/src/test/links.test.ts +++ b/src/test/links.test.ts @@ -13,7 +13,8 @@ import { Range, TextDocument, } from '../jsonLanguageService'; -import { resolve as resolvePath } from 'path'; +import * as path from 'path'; +import { URI } from 'vscode-uri'; suite('JSON Find Links', () => { const testFindLinksFor = function (value: string, expected: {offset: number, length: number, target: number} | null): PromiseLike { @@ -72,9 +73,13 @@ suite('JSON Find Links', () => { }); test('URI reference link', async function () { - const relativePath = './src/test/fixtures/uri-reference.txt'; - const content = `{"stringProp": "string-value", "uriProp": "${relativePath}", "uriPropNotFound": "./does/not/exist.txt"}`; - const document = TextDocument.create('test://test.json', 'json', 0, content); + // This test file runs in `./lib/umd/test`, but the fixtures are in `./src`. + const refRelPath = '../../../src/test/fixtures/uri-reference.txt'; + const refAbsPath = path.join(__dirname, refRelPath); + const docAbsPath = path.join(__dirname, 'test.json'); + + const content = `{"stringProp": "string-value", "uriProp": "${refRelPath}", "uriPropNotFound": "./does/not/exist.txt"}`; + const document = TextDocument.create(URI.file(docAbsPath).toString(), 'json', 0, content); const schema: JSONSchema = { type: 'object', properties: { @@ -94,11 +99,10 @@ suite('JSON Find Links', () => { await testFindLinksWithSchema(document, schema).then((links) => { assert.notDeepEqual(links, []); - const absPath = resolvePath(relativePath); - assert.equal(links[0].target, `file://${absPath}`); + assert.equal(links[0].target, URI.file(refAbsPath).toString()); - const startOffset = content.indexOf(relativePath); - const endOffset = startOffset + relativePath.length; + const startOffset = content.indexOf(refRelPath); + const endOffset = startOffset + refRelPath.length; const range = Range.create(document.positionAt(startOffset), document.positionAt(endOffset)); assert.deepEqual(links[0].range, range); }); From 5aa341a038a0f8ac68a868fda8ccc3e658e936f3 Mon Sep 17 00:00:00 2001 From: Skip Baney Date: Sun, 14 Jan 2024 22:45:48 -0600 Subject: [PATCH 4/6] restructure conditionals to reduce indentation --- src/services/jsonLinks.ts | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/services/jsonLinks.ts b/src/services/jsonLinks.ts index 7c40292f..f22678da 100644 --- a/src/services/jsonLinks.ts +++ b/src/services/jsonLinks.ts @@ -41,26 +41,28 @@ export function findLinks(document: TextDocument, doc: JSONDocument, schemaServi } if (node.type === "property" && node.valueNode?.type === 'string' && schemaService) { const pathNode = node.valueNode; - const pathLinks: DocumentLink[] = []; const promise = schemaService.getSchemaForResource(document.uri, doc).then((schema) => { - if (schema) { - const matchingSchemas = doc.getMatchingSchemas(schema.schema, pathNode.offset); - matchingSchemas.forEach((s) => { - if (s.node === pathNode && !s.inverted && s.schema) { - if (s.schema.format === 'uri-reference') { - const pathURI = resolveURIRef(pathNode.value, document); - if (pathURI) { - if (fileExistsSync(pathURI.fsPath)) { - pathLinks.push({ - target: pathURI.toString(), - range: createRange(document, pathNode) - }); - } - } - } - } - }); + const pathLinks: DocumentLink[] = []; + if (!schema) { + return pathLinks; } + doc.getMatchingSchemas(schema.schema, pathNode.offset).forEach((s) => { + if (s.node !== pathNode || s.inverted || !s.schema) { + return; + } + if (s.schema.format !== 'uri-reference') { + return; + } + const pathURI = resolveURIRef(pathNode.value, document); + if (pathURI) { + if (fileExistsSync(pathURI.fsPath)) { + pathLinks.push({ + target: pathURI.toString(), + range: createRange(document, pathNode) + }); + } + } + }); return pathLinks; }); promises.push(promise); From 802fb8375f3a1592dee7b4880b19eb2130f0a7c8 Mon Sep 17 00:00:00 2001 From: Skip Baney Date: Sun, 14 Jan 2024 22:54:05 -0600 Subject: [PATCH 5/6] remove another nested conditional --- src/services/jsonLinks.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/services/jsonLinks.ts b/src/services/jsonLinks.ts index f22678da..ab2109de 100644 --- a/src/services/jsonLinks.ts +++ b/src/services/jsonLinks.ts @@ -48,19 +48,20 @@ export function findLinks(document: TextDocument, doc: JSONDocument, schemaServi } doc.getMatchingSchemas(schema.schema, pathNode.offset).forEach((s) => { if (s.node !== pathNode || s.inverted || !s.schema) { - return; + return; // Not an _exact_ schema match. } if (s.schema.format !== 'uri-reference') { - return; + return; // Not a uri-ref. } const pathURI = resolveURIRef(pathNode.value, document); - if (pathURI) { - if (fileExistsSync(pathURI.fsPath)) { - pathLinks.push({ - target: pathURI.toString(), - range: createRange(document, pathNode) - }); - } + if (!pathURI) { + return; // Unable to resolve ref. + } + if (fileExistsSync(pathURI.fsPath)) { + pathLinks.push({ + target: pathURI.toString(), + range: createRange(document, pathNode) + }); } }); return pathLinks; From c5705dd1e43e1aebcda8d845f24419562b95128f Mon Sep 17 00:00:00 2001 From: Skip Baney Date: Sat, 10 Feb 2024 19:20:15 -0600 Subject: [PATCH 6/6] add filesystem abstraction and remove node API calls --- CHANGELOG.md | 5 +++ src/jsonLanguageService.ts | 2 +- src/jsonLanguageTypes.ts | 47 +++++++++++++++++++++ src/services/jsonLinks.ts | 72 +++++++++++++++++++-------------- src/test/links.test.ts | 6 ++- src/test/testUtil/fsProvider.ts | 52 ++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 32 deletions(-) create mode 100644 src/test/testUtil/fsProvider.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ac3d90f..190bb9f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/jsonLanguageService.ts b/src/jsonLanguageService.ts index 9124ba55..0234288f 100644 --- a/src/jsonLanguageService.ts +++ b/src/jsonLanguageService.ts @@ -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); diff --git a/src/jsonLanguageTypes.ts b/src/jsonLanguageTypes.ts index 7f4ae6f1..790f26e3 100644 --- a/src/jsonLanguageTypes.ts +++ b/src/jsonLanguageTypes.ts @@ -252,6 +252,49 @@ export interface Thenable { then(onfulfilled?: (value: R) => TResult | Thenable, onrejected?: (reason: any) => void): Thenable; } +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; +} + export interface LanguageServiceParams { /** * 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 { * 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. */ diff --git a/src/services/jsonLinks.ts b/src/services/jsonLinks.ts index ab2109de..e5090805 100644 --- a/src/services/jsonLinks.ts +++ b/src/services/jsonLinks.ts @@ -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 { - return findLinks(document, doc, this.schemaService); + return findLinks(document, doc, this.schemaService, this.fileSystemProvider); } } -export function findLinks(document: TextDocument, doc: JSONDocument, schemaService?: IJSONSchemaService): Thenable { +export function findLinks(document: TextDocument, doc: JSONDocument, schemaService?: IJSONSchemaService, fileSystemProvider?: FileSystemProvider): Thenable { const promises: Thenable[] = []; const refLinks: DocumentLink[] = []; @@ -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); } @@ -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)); } @@ -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('/')) { @@ -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); } diff --git a/src/test/links.test.ts b/src/test/links.test.ts index 87345537..faa90715 100644 --- a/src/test/links.test.ts +++ b/src/test/links.test.ts @@ -13,6 +13,7 @@ import { Range, TextDocument, } from '../jsonLanguageService'; +import { getFsProvider } from './testUtil/fsProvider'; import * as path from 'path'; import { URI } from 'vscode-uri'; @@ -38,7 +39,10 @@ suite('JSON Find Links', () => { function testFindLinksWithSchema(document: TextDocument, schema: JSONSchema): PromiseLike { 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); diff --git a/src/test/testUtil/fsProvider.ts b/src/test/testUtil/fsProvider.ts new file mode 100644 index 00000000..8b027081 --- /dev/null +++ b/src/test/testUtil/fsProvider.ts @@ -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 + }); + }); + }); + }, + }; +}