From 4dcdcabc764508cea78025eaef1d9070e2e026b6 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 25 Apr 2017 15:26:41 +0200 Subject: [PATCH 01/11] Use WHATWG URL instead of url.parse() --- package.json | 3 ++- src/project-manager.ts | 25 +++++++++++-------------- src/typescript-service.ts | 17 +++++++---------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 226ed002c..cb7cc4674 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "vscode-jsonrpc": "^3.1.0", "vscode-languageserver": "^3.1.0", "vscode-languageserver-types": "^3.0.3", + "whatwg-url": "^4.7.1", "yarn": "^0.21.3" }, "devDependencies": { @@ -61,7 +62,7 @@ "@types/lodash": "4.14.63", "@types/mocha": "2.2.41", "@types/mz": "0.0.31", - "@types/node": "7.0.14", + "@types/node": "6.0.70", "@types/object-hash": "0.5.28", "@types/rimraf": "0.0.28", "@types/sinon": "2.1.3", diff --git a/src/project-manager.ts b/src/project-manager.ts index f302e965d..2765f299a 100644 --- a/src/project-manager.ts +++ b/src/project-manager.ts @@ -4,12 +4,12 @@ import { Span } from 'opentracing'; import * as os from 'os'; import * as path_ from 'path'; import * as ts from 'typescript'; -import * as url from 'url'; import { Disposable } from 'vscode-languageserver'; import { FileSystemUpdater } from './fs'; import { Logger, NoopLogger } from './logging'; import { InMemoryFileSystem } from './memfs'; import * as util from './util'; +const URL: typeof window.URL = require('whatwg-url').URL; export type ConfigType = 'js' | 'ts'; @@ -315,22 +315,19 @@ export class ProjectManager implements Disposable { * @param uri URI of the file to process * @return URIs of files referenced by the file */ - private resolveReferencedFiles(uri: string, span = new Span()): Observable { - let observable = this.referencedFiles.get(uri); + private resolveReferencedFiles(uriStr: string, span = new Span()): Observable { + let observable = this.referencedFiles.get(uriStr); if (observable) { return observable; } - const parts = url.parse(uri); - if (!parts.pathname) { - return Observable.throw(new Error(`Invalid URI ${uri}`)); - } + const uri = new URL(uriStr); // TypeScript works with file paths, not URIs - const filePath = parts.pathname.split('/').map(decodeURIComponent).join('/'); - observable = Observable.from(this.updater.ensure(uri)) + const filePath = uri.pathname.split('/').map(decodeURIComponent).join('/'); + observable = Observable.from(this.updater.ensure(uriStr)) .mergeMap(() => { const config = this.getConfiguration(filePath); config.ensureBasicFiles(span); - const contents = this.localFs.getContent(uri); + const contents = this.localFs.getContent(uriStr); const info = ts.preProcessFile(contents, true, true); const compilerOpt = config.getHost().getCompilationSettings(); // TODO remove platform-specific behavior here, the host OS is not coupled to the client OS @@ -370,16 +367,16 @@ export class ProjectManager implements Disposable { ); }) // Use same scheme, slashes, host for referenced URI as input file - .map(filePath => url.format({ ...parts, pathname: filePath.split(/[\\\/]/).map(encodeURIComponent).join('/'), search: undefined, hash: undefined })) + .map(filePath => new URL(filePath.split(/[\\\/]/).map(encodeURIComponent).join('/'), uriStr).href) // Don't cache errors - .catch(err => { - this.referencedFiles.delete(uri); + .catch(err => { + this.referencedFiles.delete(uriStr); throw err; }) // Make sure all subscribers get the same values .publishReplay() .refCount(); - this.referencedFiles.set(uri, observable); + this.referencedFiles.set(uriStr, observable); return observable; } diff --git a/src/typescript-service.ts b/src/typescript-service.ts index 1e20d2dd5..8fc0aec80 100644 --- a/src/typescript-service.ts +++ b/src/typescript-service.ts @@ -4,7 +4,6 @@ import { toPairs } from 'lodash'; import { Span } from 'opentracing'; import * as path_ from 'path'; import * as ts from 'typescript'; -import * as url from 'url'; import { CompletionItem, CompletionItemKind, @@ -46,6 +45,7 @@ import { } from './request-type'; import * as util from './util'; import hashObject = require('object-hash'); +const URL: typeof window.URL = require('whatwg-url').URL; export interface TypeScriptServiceOptions { traceModuleResolution?: boolean; @@ -78,7 +78,7 @@ export class TypeScriptService { /** * The root URI as passed to `initialize` or converted from `rootPath` */ - protected rootUri: string; + protected rootUri: URL; /** * Cached response for empty workspace/symbol query @@ -134,7 +134,7 @@ export class TypeScriptService { async initialize(params: InitializeParams, span = new Span()): Promise { if (params.rootUri || params.rootPath) { this.root = params.rootPath || util.uri2path(params.rootUri!); - this.rootUri = params.rootUri || util.path2uri('', params.rootPath!); + this.rootUri = new URL(params.rootUri || util.path2uri('', params.rootPath!)); this._initializeFileSystems(!this.options.strict && !(params.capabilities.xcontentProvider && params.capabilities.xfilesProvider)); this.updater = new FileSystemUpdater(this.fileSystem, this.inMemoryFileSystem); this.projectManager = new pm.ProjectManager(this.root, this.inMemoryFileSystem, this.updater, !!this.options.strict, this.traceModuleResolution, this.logger); @@ -142,8 +142,7 @@ export class TypeScriptService { this.isDefinitelyTyped = (async () => { try { // Fetch root package.json (if exists) - const rootUriParts = url.parse(this.rootUri); - const packageJsonUri = url.format({ ...rootUriParts, pathname: path_.posix.join(rootUriParts.pathname || '', 'package.json') }); + const packageJsonUri = new URL('package.json', this.rootUri.href).href; await this.updater.ensure(packageJsonUri); // Check name const packageJson = JSON.parse(this.inMemoryFileSystem.getContent(packageJsonUri)); @@ -488,14 +487,12 @@ export class TypeScriptService { } // Fetch all files in the package subdirectory - const rootUriParts = url.parse(this.rootUri); // All packages are in the types/ subdirectory - const packageRoot = path_.posix.join(rootUriParts.pathname || '', params.symbol.package.name.substr(1)) + '/'; - const packageRootUri = url.format({ ...rootUriParts, pathname: packageRoot, search: undefined, hash: undefined }); + const packageRootUri = new URL(params.symbol.package.name.substr(1), this.rootUri.href); await this.updater.ensureStructure(span); await Promise.all( iterate(this.inMemoryFileSystem.uris()) - .filter(uri => uri.startsWith(packageRootUri)) + .filter(uri => uri.startsWith(packageRootUri.href)) .map(uri => this.updater.ensure(uri, span)) ); this.projectManager.createConfigurations(); @@ -503,7 +500,7 @@ export class TypeScriptService { // Search symbol in configuration // forcing TypeScript mode - const config = this.projectManager.getConfiguration(packageRoot, 'ts'); + const config = this.projectManager.getConfiguration(util.uri2path(packageRootUri.href), 'ts'); return Array.from(this._collectWorkspaceSymbols(config, params.query || symbolQuery, params.limit)); } catch (err) { span.setTag('error', true); From 574b313f174f4d7b43ed5fecb664c1f7f2436fec Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Tue, 25 Apr 2017 23:48:04 +0200 Subject: [PATCH 02/11] Use URL for most functions --- src/fs.ts | 68 ++++++++++++++------------- src/memfs.ts | 32 +++++++------ src/project-manager.ts | 49 ++++++++++---------- src/test/fs-helpers.ts | 10 ++-- src/test/fs.test.ts | 20 ++++---- src/test/project-manager.test.ts | 9 ++-- src/typescript-service.ts | 79 ++++++++++++++++++-------------- src/typings/whatwg-url.d.ts | 4 ++ src/util.ts | 27 +++++++---- 9 files changed, 164 insertions(+), 134 deletions(-) create mode 100644 src/typings/whatwg-url.d.ts diff --git a/src/fs.ts b/src/fs.ts index 729817c61..3941a25e4 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -1,12 +1,12 @@ import * as fs from 'mz/fs'; -import * as path from 'path'; import { LanguageClient } from './lang-handler'; import glob = require('glob'); import iterate from 'iterare'; import { Span } from 'opentracing'; import Semaphore from 'semaphore-async-await'; +import { URL } from 'whatwg-url'; import { InMemoryFileSystem } from './memfs'; -import { normalizeDir, path2uri, toUnixPath, uri2path } from './util'; +import { uri2path } from './util'; export interface FileSystem { /** @@ -15,7 +15,7 @@ export interface FileSystem { * @param base A URI under which to search, resolved relative to the rootUri * @return A promise that is fulfilled with an array of URIs */ - getWorkspaceFiles(base?: string, childOf?: Span): Promise>; + getWorkspaceFiles(base?: URL, childOf?: Span): Promise>; /** * Returns the content of a text document @@ -23,7 +23,7 @@ export interface FileSystem { * @param uri The URI of the text document, resolved relative to the rootUri * @return A promise that is fulfilled with the text document content */ - getTextDocumentContent(uri: string, childOf?: Span): Promise; + getTextDocumentContent(uri: URL, childOf?: Span): Promise; } export class RemoteFileSystem implements FileSystem { @@ -34,46 +34,48 @@ export class RemoteFileSystem implements FileSystem { * The files request is sent from the server to the client to request a list of all files in the workspace or inside the directory of the base parameter, if given. * A language server can use the result to index files by filtering and doing a content request for each text document of interest. */ - async getWorkspaceFiles(base?: string, childOf = new Span()): Promise> { - return iterate(await this.client.workspaceXfiles({ base }, childOf)) - .map(textDocument => textDocument.uri); + async getWorkspaceFiles(base?: URL, childOf = new Span()): Promise> { + return iterate(await this.client.workspaceXfiles({ base: base && base.href }, childOf)) + .map(textDocument => new URL(textDocument.uri)); } /** * The content request is sent from the server to the client to request the current content of any text document. This allows language servers to operate without accessing the file system directly. */ - async getTextDocumentContent(uri: string, childOf = new Span()): Promise { - const textDocument = await this.client.textDocumentXcontent({ textDocument: { uri } }, childOf); + async getTextDocumentContent(uri: URL, childOf = new Span()): Promise { + const textDocument = await this.client.textDocumentXcontent({ textDocument: { uri: uri.href } }, childOf); return textDocument.text; } } +/** + * FileSystem implementation that reads from the local disk + */ export class LocalFileSystem implements FileSystem { /** - * @param rootPath The root directory path that relative URIs should be resolved to - */ - constructor(private rootPath: string) {} - - /** - * Converts the URI to an absolute path + * @param rootUri The workspace root URI that is used when no base is given */ - protected resolveUriToPath(uri: string): string { - return toUnixPath(path.resolve(this.rootPath, uri2path(uri))); - } + constructor(private rootUri: URL) {} - async getWorkspaceFiles(base?: string): Promise> { - // Even if no base provided, still need to call resolveUriToPath which may be overridden - const root = this.resolveUriToPath(base || path2uri('', this.rootPath)); - const baseUri = path2uri('', normalizeDir(root)) + '/'; + async getWorkspaceFiles(base: URL = this.rootUri): Promise> { const files = await new Promise((resolve, reject) => { - glob('*', { cwd: root, nodir: true, matchBase: true }, (err, matches) => err ? reject(err) : resolve(matches)); + glob('*', { + // Search the base directory + cwd: uri2path(base), + // Don't return directories + nodir: true, + // Search directories recursively + matchBase: true, + // Return absolute file paths + absolute: true + } as any, (err, matches) => err ? reject(err) : resolve(matches)); }); - return iterate(files).map(file => baseUri + file.split('/').map(encodeURIComponent).join('/')); + return iterate(files).map(file => new URL(file.split('/').map(encodeURIComponent).join('/'), base.href + '/')); } - async getTextDocumentContent(uri: string): Promise { - return fs.readFile(this.resolveUriToPath(uri), 'utf8'); + async getTextDocumentContent(uri: URL): Promise { + return fs.readFile(uri2path(uri), 'utf8'); } } @@ -107,7 +109,7 @@ export class FileSystemUpdater { * @param uri URI of the file to fetch * @param childOf A parent span for tracing */ - async fetch(uri: string, childOf = new Span()): Promise { + async fetch(uri: URL, childOf = new Span()): Promise { // Limit concurrent fetches const promise = this.concurrencyLimit.execute(async () => { try { @@ -115,11 +117,11 @@ export class FileSystemUpdater { this.inMemoryFs.add(uri, content); this.inMemoryFs.getContent(uri); } catch (err) { - this.fetches.delete(uri); + this.fetches.delete(uri.href); throw err; } }); - this.fetches.set(uri, promise); + this.fetches.set(uri.href, promise); return promise; } @@ -130,8 +132,8 @@ export class FileSystemUpdater { * @param uri URI of the file to ensure * @param span An OpenTracing span for tracing */ - ensure(uri: string, span = new Span()): Promise { - return this.fetches.get(uri) || this.fetch(uri, span); + ensure(uri: URL, span = new Span()): Promise { + return this.fetches.get(uri.href) || this.fetch(uri, span); } /** @@ -176,8 +178,8 @@ export class FileSystemUpdater { * * @param uri URI of the file that changed */ - invalidate(uri: string): void { - this.fetches.delete(uri); + invalidate(uri: URL): void { + this.fetches.delete(uri.href); } /** diff --git a/src/memfs.ts b/src/memfs.ts index 19bdf22d7..77b280577 100644 --- a/src/memfs.ts +++ b/src/memfs.ts @@ -1,6 +1,8 @@ import * as fs_ from 'fs'; +import iterate from 'iterare'; import * as path_ from 'path'; import * as ts from 'typescript'; +import { URL } from 'whatwg-url'; import { Logger, NoopLogger } from './logging'; import * as match from './match-files'; import * as util from './util'; @@ -53,8 +55,8 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti /** * Returns an IterableIterator for all URIs known to exist in the workspace (content loaded or not) */ - uris(): IterableIterator { - return this.files.keys(); + uris(): IterableIterator { + return iterate(this.files.keys()).map(uri => new URL(uri)); } /** @@ -63,10 +65,10 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti * @param uri The URI of the file * @param content The optional content */ - add(uri: string, content?: string): void { + add(uri: URL, content?: string): void { // Make sure not to override existing content with undefined - if (content !== undefined || !this.files.has(uri)) { - this.files.set(uri, content); + if (content !== undefined || !this.files.has(uri.href)) { + this.files.set(uri.href, content); } // Add to directory tree const filePath = util.uri2path(uri); @@ -93,8 +95,8 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti * * @param uri URI to a file */ - has(uri: string): boolean { - return this.files.has(uri) || this.fileExists(util.uri2path(uri)); + has(uri: URL): boolean { + return this.files.has(uri.href) || this.fileExists(util.uri2path(uri)); } /** @@ -104,8 +106,8 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti * * TODO take overlay into account */ - getContent(uri: string): string { - let content = this.files.get(uri); + getContent(uri: URL): string { + let content = this.files.get(uri.href); if (content === undefined) { content = typeScriptLibraries.get(util.uri2path(uri)); } @@ -160,16 +162,16 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti * Invalidates temporary content denoted by the given URI * @param uri file's URI */ - didClose(uri: string) { - this.overlay.delete(uri); + didClose(uri: URL) { + this.overlay.delete(uri.href); } /** * Adds temporary content denoted by the given URI * @param uri file's URI */ - didSave(uri: string) { - const content = this.overlay.get(uri); + didSave(uri: URL) { + const content = this.overlay.get(uri.href); if (content !== undefined) { this.add(uri, content); } @@ -179,8 +181,8 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti * Updates temporary content denoted by the given URI * @param uri file's URI */ - didChange(uri: string, text: string) { - this.overlay.set(uri, text); + didChange(uri: URL, text: string) { + this.overlay.set(uri.href, text); } /** diff --git a/src/project-manager.ts b/src/project-manager.ts index 2765f299a..a8478966d 100644 --- a/src/project-manager.ts +++ b/src/project-manager.ts @@ -5,11 +5,11 @@ import * as os from 'os'; import * as path_ from 'path'; import * as ts from 'typescript'; import { Disposable } from 'vscode-languageserver'; +import { URL } from 'whatwg-url'; import { FileSystemUpdater } from './fs'; import { Logger, NoopLogger } from './logging'; import { InMemoryFileSystem } from './memfs'; import * as util from './util'; -const URL: typeof window.URL = require('whatwg-url').URL; export type ConfigType = 'js' | 'ts'; @@ -83,7 +83,7 @@ export class ProjectManager implements Disposable { /** * A URI Map from file to files referenced by the file, so files only need to be pre-processed once */ - private referencedFiles = new Map>(); + private referencedFiles = new Map>(); /** * @param rootPath root path as passed to `initialize` @@ -155,7 +155,7 @@ export class ProjectManager implements Disposable { // Ensure content of all all global .d.ts, [tj]sconfig.json, package.json files await Promise.all( iterate(this.localFs.uris()) - .filter(uri => util.isGlobalTSFile(uri) || util.isConfigFile(uri) || util.isPackageJsonFile(uri)) + .filter(uri => util.isGlobalTSFile(uri.pathname) || util.isConfigFile(uri.pathname) || util.isPackageJsonFile(uri.pathname)) .map(uri => this.updater.ensure(uri)) ); // Scan for [tj]sconfig.json files @@ -201,7 +201,7 @@ export class ProjectManager implements Disposable { await this.updater.ensureStructure(span); await Promise.all( iterate(this.localFs.uris()) - .filter(uri => !uri.includes('/node_modules/') && util.isJSTSFile(uri) || util.isConfigFile(uri) || util.isPackageJsonFile(uri)) + .filter(uri => !uri.pathname.includes('/node_modules/') && util.isJSTSFile(uri.pathname) || util.isConfigFile(uri.pathname) || util.isPackageJsonFile(uri.pathname)) .map(uri => this.updater.ensure(uri)) ); this.createConfigurations(); @@ -232,7 +232,7 @@ export class ProjectManager implements Disposable { await this.updater.ensureStructure(span); await Promise.all( iterate(this.localFs.uris()) - .filter(uri => util.isJSTSFile(uri) || util.isConfigFile(uri) || util.isPackageJsonFile(uri)) + .filter(uri => util.isJSTSFile(uri.pathname) || util.isConfigFile(uri.pathname) || util.isPackageJsonFile(uri.pathname)) .map(uri => this.updater.ensure(uri)) ); this.createConfigurations(); @@ -265,15 +265,15 @@ export class ProjectManager implements Disposable { * @param childOf OpenTracing parent span for tracing * @return Observable of file URIs ensured */ - ensureReferencedFiles(uri: string, maxDepth = 30, ignore = new Set(), childOf = new Span()): Observable { + ensureReferencedFiles(uri: URL, maxDepth = 30, ignore = new Set(), childOf = new Span()): Observable { const span = childOf.tracer().startSpan('Ensure referenced files', { childOf }); span.addTags({ uri, maxDepth }); - ignore.add(uri); + ignore.add(uri.href); return Observable.from(this.ensureModuleStructure(span)) // If max depth was reached, don't go any further .mergeMap(() => maxDepth === 0 ? [] : this.resolveReferencedFiles(uri, span)) // Prevent cycles - .filter(referencedUri => !ignore.has(referencedUri)) + .filter(referencedUri => !ignore.has(referencedUri.href)) // Call method recursively with one less dep level .mergeMap(referencedUri => this.ensureReferencedFiles(referencedUri, maxDepth - 1, ignore) @@ -315,19 +315,18 @@ export class ProjectManager implements Disposable { * @param uri URI of the file to process * @return URIs of files referenced by the file */ - private resolveReferencedFiles(uriStr: string, span = new Span()): Observable { - let observable = this.referencedFiles.get(uriStr); + private resolveReferencedFiles(uri: URL, span = new Span()): Observable { + let observable = this.referencedFiles.get(uri.href); if (observable) { return observable; } - const uri = new URL(uriStr); // TypeScript works with file paths, not URIs const filePath = uri.pathname.split('/').map(decodeURIComponent).join('/'); - observable = Observable.from(this.updater.ensure(uriStr)) + observable = Observable.from(this.updater.ensure(uri)) .mergeMap(() => { const config = this.getConfiguration(filePath); config.ensureBasicFiles(span); - const contents = this.localFs.getContent(uriStr); + const contents = this.localFs.getContent(uri); const info = ts.preProcessFile(contents, true, true); const compilerOpt = config.getHost().getCompilationSettings(); // TODO remove platform-specific behavior here, the host OS is not coupled to the client OS @@ -367,16 +366,16 @@ export class ProjectManager implements Disposable { ); }) // Use same scheme, slashes, host for referenced URI as input file - .map(filePath => new URL(filePath.split(/[\\\/]/).map(encodeURIComponent).join('/'), uriStr).href) + .map(filePath => new URL(filePath.split(/[\\\/]/).map(encodeURIComponent).join('/'), uri.href)) // Don't cache errors - .catch(err => { - this.referencedFiles.delete(uriStr); + .catch(err => { + this.referencedFiles.delete(uri.href); throw err; }) // Make sure all subscribers get the same values .publishReplay() .refCount(); - this.referencedFiles.set(uriStr, observable); + this.referencedFiles.set(uri.href, observable); return observable; } @@ -425,7 +424,7 @@ export class ProjectManager implements Disposable { * @param uri file's URI * @param text file's content */ - didOpen(uri: string, text: string) { + didOpen(uri: URL, text: string) { this.didChange(uri, text); } @@ -433,11 +432,11 @@ export class ProjectManager implements Disposable { * Called when file was closed by client. Current implementation invalidates compiled version * @param uri file's URI */ - didClose(uri: string, span = new Span()) { + didClose(uri: URL, span = new Span()) { const filePath = util.uri2path(uri); this.localFs.didClose(uri); - let version = this.versions.get(uri) || 0; - this.versions.set(uri, ++version); + let version = this.versions.get(uri.href) || 0; + this.versions.set(uri.href, ++version); const config = this.getConfigurationIfExists(filePath); if (!config) { return; @@ -452,11 +451,11 @@ export class ProjectManager implements Disposable { * @param uri file's URI * @param text file's content */ - didChange(uri: string, text: string, span = new Span()) { + didChange(uri: URL, text: string, span = new Span()) { const filePath = util.uri2path(uri); this.localFs.didChange(uri, text); - let version = this.versions.get(uri) || 0; - this.versions.set(uri, ++version); + let version = this.versions.get(uri.href) || 0; + this.versions.set(uri.href, ++version); const config = this.getConfigurationIfExists(filePath); if (!config) { return; @@ -470,7 +469,7 @@ export class ProjectManager implements Disposable { * Called when file was saved by client * @param uri file's URI */ - didSave(uri: string) { + didSave(uri: URL) { this.localFs.didSave(uri); } diff --git a/src/test/fs-helpers.ts b/src/test/fs-helpers.ts index 1e9b3c1ca..576adfe82 100644 --- a/src/test/fs-helpers.ts +++ b/src/test/fs-helpers.ts @@ -1,4 +1,5 @@ import { iterate } from 'iterare'; +import { URL } from 'whatwg-url'; import { FileSystem } from '../fs'; /** @@ -8,13 +9,14 @@ export class MapFileSystem implements FileSystem { constructor(private files: Map) { } - async getWorkspaceFiles(base?: string): Promise> { + async getWorkspaceFiles(base?: URL): Promise> { return iterate(this.files.keys()) - .filter(path => !base || path.startsWith(base)); + .filter(uri => !base || uri.startsWith(base.href)) + .map(uri => new URL(uri)); } - async getTextDocumentContent(uri: string): Promise { - const ret = this.files.get(uri); + async getTextDocumentContent(uri: URL): Promise { + const ret = this.files.get(uri.href); if (ret === undefined) { throw new Error(`Attempt to read not-existent file ${uri}`); } diff --git a/src/test/fs.test.ts b/src/test/fs.test.ts index b43bc57db..96d76bef1 100644 --- a/src/test/fs.test.ts +++ b/src/test/fs.test.ts @@ -4,8 +4,9 @@ import * as fs from 'mz/fs'; import * as path from 'path'; import * as rimraf from 'rimraf'; import * as temp from 'temp'; +import { URL } from 'whatwg-url'; import { LocalFileSystem } from '../fs'; -import { path2uri, toUnixPath } from '../util'; +import { path2uri } from '../util'; import chaiAsPromised = require('chai-as-promised'); chai.use(chaiAsPromised); const assert = chai.assert; @@ -14,13 +15,13 @@ describe('fs.ts', () => { describe('LocalFileSystem', () => { let temporaryDir: string; let fileSystem: LocalFileSystem; - let baseUri: string; + let baseUri: URL; before(async () => { temporaryDir = await new Promise((resolve, reject) => { temp.mkdir('local-fs', (err: Error, dirPath: string) => err ? reject(err) : resolve(dirPath)); }); - baseUri = path2uri('', temporaryDir) + '/'; + baseUri = new URL(path2uri('', temporaryDir) + '/'); await fs.mkdir(path.join(temporaryDir, 'foo')); await fs.mkdir(path.join(temporaryDir, '@types')); await fs.mkdir(path.join(temporaryDir, '@types', 'diff')); @@ -28,7 +29,7 @@ describe('fs.ts', () => { await fs.writeFile(path.join(temporaryDir, 'tweedledum'), 'bye'); await fs.writeFile(path.join(temporaryDir, 'foo', 'bar.ts'), 'baz'); await fs.writeFile(path.join(temporaryDir, '@types', 'diff', 'index.d.ts'), 'baz'); - fileSystem = new LocalFileSystem(toUnixPath(temporaryDir)); + fileSystem = new LocalFileSystem(baseUri); }); after(async () => { await new Promise((resolve, reject) => { @@ -38,7 +39,8 @@ describe('fs.ts', () => { describe('getWorkspaceFiles()', () => { it('should return all files in the workspace', async () => { - assert.sameMembers(iterate(await fileSystem.getWorkspaceFiles()).toArray(), [ + const files = iterate(await fileSystem.getWorkspaceFiles()).map(uri => uri.href).toArray(); + assert.sameMembers(files, [ baseUri + 'tweedledee', baseUri + 'tweedledum', baseUri + 'foo/bar.ts', @@ -46,17 +48,15 @@ describe('fs.ts', () => { ]); }); it('should return all files under specific root', async () => { - assert.sameMembers(iterate(await fileSystem.getWorkspaceFiles(baseUri + 'foo')).toArray(), [ + const files = iterate(await fileSystem.getWorkspaceFiles(new URL('foo/', baseUri.href))).map(uri => uri.href).toArray(); + assert.sameMembers(files, [ baseUri + 'foo/bar.ts' ]); }); }); describe('getTextDocumentContent()', () => { - it('should read files denoted by relative URI', async () => { - assert.equal(await fileSystem.getTextDocumentContent('tweedledee'), 'hi'); - }); it('should read files denoted by absolute URI', async () => { - assert.equal(await fileSystem.getTextDocumentContent(baseUri + 'tweedledee'), 'hi'); + assert.equal(await fileSystem.getTextDocumentContent(new URL('tweedledee', baseUri.href)), 'hi'); }); }); }); diff --git a/src/test/project-manager.test.ts b/src/test/project-manager.test.ts index b37aa960b..150fb7f10 100644 --- a/src/test/project-manager.test.ts +++ b/src/test/project-manager.test.ts @@ -2,6 +2,7 @@ import * as chai from 'chai'; import chaiAsPromised = require('chai-as-promised'); chai.use(chaiAsPromised); const assert = chai.assert; +import { URL } from 'whatwg-url'; import { FileSystemUpdater } from '../fs'; import { InMemoryFileSystem } from '../memfs'; import { ProjectManager } from '../project-manager'; @@ -46,10 +47,10 @@ describe('ProjectManager', () => { projectManager = new ProjectManager('/', memfs, updater, true); }); it('should ensure content for imports and references is fetched', async () => { - await projectManager.ensureReferencedFiles('file:///src/dummy.ts').toPromise(); - memfs.getContent('file:///node_modules/somelib/index.js'); - memfs.getContent('file:///node_modules/somelib/pathref.d.ts'); - memfs.getContent('file:///node_modules/%40types/node/index.d.ts'); + await projectManager.ensureReferencedFiles(new URL('file:///src/dummy.ts')).toPromise(); + memfs.getContent(new URL('file:///node_modules/somelib/index.js')); + memfs.getContent(new URL('file:///node_modules/somelib/pathref.d.ts')); + memfs.getContent(new URL('file:///node_modules/%40types/node/index.d.ts')); }); }); describe('getConfiguration()', () => { diff --git a/src/typescript-service.ts b/src/typescript-service.ts index 8fc0aec80..e2618ecbc 100644 --- a/src/typescript-service.ts +++ b/src/typescript-service.ts @@ -45,7 +45,7 @@ import { } from './request-type'; import * as util from './util'; import hashObject = require('object-hash'); -const URL: typeof window.URL = require('whatwg-url').URL; +import { URL } from 'whatwg-url'; export interface TypeScriptServiceOptions { traceModuleResolution?: boolean; @@ -133,8 +133,8 @@ export class TypeScriptService { */ async initialize(params: InitializeParams, span = new Span()): Promise { if (params.rootUri || params.rootPath) { - this.root = params.rootPath || util.uri2path(params.rootUri!); this.rootUri = new URL(params.rootUri || util.path2uri('', params.rootPath!)); + this.root = params.rootPath || util.uri2path(this.rootUri); this._initializeFileSystems(!this.options.strict && !(params.capabilities.xcontentProvider && params.capabilities.xfilesProvider)); this.updater = new FileSystemUpdater(this.fileSystem, this.inMemoryFileSystem); this.projectManager = new pm.ProjectManager(this.root, this.inMemoryFileSystem, this.updater, !!this.options.strict, this.traceModuleResolution, this.logger); @@ -142,7 +142,7 @@ export class TypeScriptService { this.isDefinitelyTyped = (async () => { try { // Fetch root package.json (if exists) - const packageJsonUri = new URL('package.json', this.rootUri.href).href; + const packageJsonUri = new URL('package.json', this.rootUri.href); await this.updater.ensure(packageJsonUri); // Check name const packageJson = JSON.parse(this.inMemoryFileSystem.getContent(packageJsonUri)); @@ -193,7 +193,7 @@ export class TypeScriptService { * @param accessDisk Whether the language server is allowed to access the local file system */ protected _initializeFileSystems(accessDisk: boolean): void { - this.fileSystem = accessDisk ? new LocalFileSystem(util.uri2path(this.root)) : new RemoteFileSystem(this.client); + this.fileSystem = accessDisk ? new LocalFileSystem(this.rootUri) : new RemoteFileSystem(this.client); this.inMemoryFileSystem = new InMemoryFileSystem(this.root); } @@ -214,11 +214,12 @@ export class TypeScriptService { async textDocumentDefinition(params: TextDocumentPositionParams, span = new Span()): Promise { const line = params.position.line; const column = params.position.character; + const uri = new URL(params.textDocument.uri); // Fetch files needed to resolve definition - await this.projectManager.ensureReferencedFiles(params.textDocument.uri, undefined, undefined, span).toPromise(); + await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const fileName: string = util.uri2path(params.textDocument.uri); + const fileName: string = util.uri2path(uri); const configuration = this.projectManager.getConfiguration(fileName); configuration.ensureBasicFiles(span); @@ -258,12 +259,13 @@ export class TypeScriptService { * know some information about it. */ textDocumentXdefinition(params: TextDocumentPositionParams, span = new Span()): Observable { + const uri = new URL(params.textDocument.uri); // Ensure files needed to resolve SymbolLocationInformation are fetched - return this.projectManager.ensureReferencedFiles(params.textDocument.uri, undefined, undefined, span) + return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) .toArray() .mergeMap(() => { // Convert URI to file path - const fileName: string = util.uri2path(params.textDocument.uri); + const fileName: string = util.uri2path(uri); // Get closest tsconfig configuration const configuration = this.projectManager.getConfiguration(fileName); configuration.ensureBasicFiles(span); @@ -305,17 +307,18 @@ export class TypeScriptService { * given text document position. */ async textDocumentHover(params: TextDocumentPositionParams, span = new Span()): Promise { + const uri = new URL(params.textDocument.uri); // Ensure files needed to resolve hover are fetched - await this.projectManager.ensureReferencedFiles(params.textDocument.uri, undefined, undefined, span).toPromise(); + await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const fileName: string = util.uri2path(params.textDocument.uri); + const fileName: string = util.uri2path(uri); const configuration = this.projectManager.getConfiguration(fileName); configuration.ensureBasicFiles(span); const sourceFile = this._getSourceFile(configuration, fileName, span); if (!sourceFile) { - throw new Error(`Unknown text document ${params.textDocument.uri}`); + throw new Error(`Expected source file ${fileName} to exist`); } const offset: number = ts.getPositionOfLineAndCharacter(sourceFile, params.position.line, params.position.character); const info = configuration.getService().getQuickInfoAtPosition(fileName, offset); @@ -343,11 +346,12 @@ export class TypeScriptService { * Returns all references to the symbol at the position in the own workspace, including references inside node_modules. */ textDocumentReferences(params: ReferenceParams, span = new Span()): Observable { + const uri = new URL(params.textDocument.uri); // Ensure all files were fetched to collect all references return Observable.from(this.projectManager.ensureOwnFiles(span)) .mergeMap(() => { // Convert URI to file path because TypeScript doesn't work with URIs - const fileName = util.uri2path(params.textDocument.uri); + const fileName = util.uri2path(uri); // Get tsconfig configuration for requested file const configuration = this.projectManager.getConfiguration(fileName); // Ensure all files have been added @@ -492,7 +496,7 @@ export class TypeScriptService { await this.updater.ensureStructure(span); await Promise.all( iterate(this.inMemoryFileSystem.uris()) - .filter(uri => uri.startsWith(packageRootUri.href)) + .filter(uri => uri.href.startsWith(packageRootUri.href)) .map(uri => this.updater.ensure(uri, span)) ); this.projectManager.createConfigurations(); @@ -500,7 +504,7 @@ export class TypeScriptService { // Search symbol in configuration // forcing TypeScript mode - const config = this.projectManager.getConfiguration(util.uri2path(packageRootUri.href), 'ts'); + const config = this.projectManager.getConfiguration(util.uri2path(packageRootUri), 'ts'); return Array.from(this._collectWorkspaceSymbols(config, params.query || symbolQuery, params.limit)); } catch (err) { span.setTag('error', true); @@ -516,11 +520,12 @@ export class TypeScriptService { * in a given text document. */ async textDocumentDocumentSymbol(params: DocumentSymbolParams, span = new Span()): Promise { + const uri = new URL(params.textDocument.uri); // Ensure files needed to resolve symbols are fetched - await this.projectManager.ensureReferencedFiles(params.textDocument.uri, undefined, undefined, span).toPromise(); + await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const fileName = util.uri2path(params.textDocument.uri); + const fileName = util.uri2path(uri); const config = this.projectManager.getConfiguration(fileName); config.ensureBasicFiles(span); @@ -599,9 +604,9 @@ export class TypeScriptService { // Ensure package.json files return Observable.from(this.projectManager.ensureModuleStructure(span)) // Iterate all files - .mergeMap(() => this.inMemoryFileSystem.uris() as any) + .mergeMap(() => this.inMemoryFileSystem.uris() as any) // Filter own package.jsons - .filter(uri => uri.includes('/package.json') && !uri.includes('/node_modules/')) + .filter(uri => uri.pathname.includes('/package.json') && !uri.pathname.includes('/node_modules/')) // Map to contents of package.jsons .mergeMap(uri => Observable.from(this.updater.ensure(uri)) @@ -647,9 +652,9 @@ export class TypeScriptService { // Ensure package.json files return Observable.from(this.projectManager.ensureModuleStructure()) // Iterate all files - .mergeMap(() => this.inMemoryFileSystem.uris() as any) + .mergeMap(() => this.inMemoryFileSystem.uris() as any) // Filter own package.jsons - .filter(uri => uri.includes('/package.json') && !uri.includes('/node_modules/')) + .filter(uri => uri.pathname.includes('/package.json') && !uri.pathname.includes('/node_modules/')) // Ensure contents of own package.jsons .mergeMap(uri => Observable.from(this.updater.ensure(uri)) @@ -688,11 +693,12 @@ export class TypeScriptService { * property filled in. */ async textDocumentCompletion(params: TextDocumentPositionParams, span = new Span()): Promise { + const uri = new URL(params.textDocument.uri); // Ensure files needed to suggest completions are fetched - await this.projectManager.ensureReferencedFiles(params.textDocument.uri, undefined, undefined, span).toPromise(); + await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const fileName: string = util.uri2path(params.textDocument.uri); + const fileName: string = util.uri2path(uri); const configuration = this.projectManager.getConfiguration(fileName); configuration.ensureBasicFiles(span); @@ -732,11 +738,12 @@ export class TypeScriptService { * information at a given cursor position. */ async textDocumentSignatureHelp(params: TextDocumentPositionParams, span = new Span()): Promise { + const uri = new URL(params.textDocument.uri); // Ensure files needed to resolve signature are fetched - await this.projectManager.ensureReferencedFiles(params.textDocument.uri, undefined, undefined, span).toPromise(); + await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const filePath = util.uri2path(params.textDocument.uri); + const filePath = util.uri2path(uri); const configuration = this.projectManager.getConfiguration(filePath); configuration.ensureBasicFiles(span); @@ -774,11 +781,12 @@ export class TypeScriptService { * to read the document's truth using the document's uri. */ async textDocumentDidOpen(params: DidOpenTextDocumentParams): Promise { + const uri = new URL(params.textDocument.uri); // Ensure files needed for most operations are fetched - await this.projectManager.ensureReferencedFiles(params.textDocument.uri).toPromise(); + await this.projectManager.ensureReferencedFiles(uri).toPromise(); - this.projectManager.didOpen(params.textDocument.uri, params.textDocument.text); + this.projectManager.didOpen(uri, params.textDocument.text); } /** @@ -787,17 +795,18 @@ export class TypeScriptService { * and language ids. */ async textDocumentDidChange(params: DidChangeTextDocumentParams): Promise { + const uri = new URL(params.textDocument.uri); let text = null; - params.contentChanges.forEach(change => { + for (const change of params.contentChanges) { if (change.range || change.rangeLength) { - throw new Error('incremental updates in textDocument/didChange not supported for file ' + params.textDocument.uri); + throw new Error('Incremental updates in textDocument/didChange not supported for file ' + uri); } text = change.text; - }); + } if (!text) { return; } - this.projectManager.didChange(params.textDocument.uri, text); + this.projectManager.didChange(uri, text); } /** @@ -805,10 +814,11 @@ export class TypeScriptService { * saved in the client. */ async textDocumentDidSave(params: DidSaveTextDocumentParams): Promise { + const uri = new URL(params.textDocument.uri); // Ensure files needed to suggest completions are fetched - await this.projectManager.ensureReferencedFiles(params.textDocument.uri).toPromise(); - this.projectManager.didSave(params.textDocument.uri); + await this.projectManager.ensureReferencedFiles(uri).toPromise(); + this.projectManager.didSave(uri); } /** @@ -817,11 +827,12 @@ export class TypeScriptService { * (e.g. if the document's uri is a file uri the truth now exists on disk). */ async textDocumentDidClose(params: DidCloseTextDocumentParams): Promise { + const uri = new URL(params.textDocument.uri); // Ensure files needed to suggest completions are fetched - await this.projectManager.ensureReferencedFiles(params.textDocument.uri).toPromise(); + await this.projectManager.ensureReferencedFiles(uri).toPromise(); - this.projectManager.didClose(params.textDocument.uri); + this.projectManager.didClose(uri); } /** diff --git a/src/typings/whatwg-url.d.ts b/src/typings/whatwg-url.d.ts new file mode 100644 index 000000000..a75adbcd7 --- /dev/null +++ b/src/typings/whatwg-url.d.ts @@ -0,0 +1,4 @@ + +declare module 'whatwg-url' { + export const URL: typeof window.URL; +} diff --git a/src/util.ts b/src/util.ts index 9f08c677e..9e2de24b6 100644 --- a/src/util.ts +++ b/src/util.ts @@ -94,17 +94,26 @@ export function path2uri(root: string, file: string): string { return ret + p; } -export function uri2path(uri: string): string { - if (uri.startsWith('file://')) { - uri = uri.substring('file://'.length); - if (process.platform === 'win32') { - if (!strict) { - uri = uri.substring(1); - } +/** + * Returns the path component of the passed URI as a file path. + * The OS style and seperator is determined by the presence of a Windows drive letter + colon in the URI. + * Does not check the URI protocol. + */ +export function uri2path(uri: URL): string { + // %-decode parts + const parts = uri.pathname.split('/').map(part => { + try { + return decodeURIComponent(part); + } catch (err) { + throw new Error(`Error decoding ${part} of ${uri}: ${err.message}`); } - uri = uri.split('/').map(decodeURIComponent).join('/'); + }); + // Strip the leading slash on Windows + const isWindowsUri = parts[0] && /^\/[a-z]:\//.test(parts[0]); + if (isWindowsUri) { + parts[0] = parts[0].substr(1); } - return uri; + return parts.join(isWindowsUri ? '\\' : '/'); } export function isLocalUri(uri: string): boolean { From cea66d7fa42ebbb70c67d16c641bc6b4079edb79 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 27 Apr 2017 17:48:49 +0200 Subject: [PATCH 03/11] Pass rootUri --- src/memfs.ts | 23 +++++++++++-------- src/project-manager.ts | 39 +++++++++++++++----------------- src/test/fs.test.ts | 2 +- src/test/memfs.test.ts | 7 +++--- src/test/project-manager.test.ts | 12 +++++----- src/typescript-service.ts | 24 ++++++++++---------- src/util.ts | 25 ++++++++++---------- 7 files changed, 66 insertions(+), 66 deletions(-) diff --git a/src/memfs.ts b/src/memfs.ts index 77b280577..af4ad19cc 100644 --- a/src/memfs.ts +++ b/src/memfs.ts @@ -46,7 +46,10 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti */ rootNode: FileSystemNode; - constructor(path: string, private logger: Logger = new NoopLogger()) { + /** + * @param rootUri The workspace root URI + */ + constructor(private rootUri: URL, path: string, private logger: Logger = new NoopLogger()) { this.path = path; this.overlay = new Map(); this.rootNode = { file: false, children: new Map() }; @@ -120,10 +123,10 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti /** * Tells if a file denoted by the given name exists in the workspace (does not have to be loaded) * - * @param path File path or URI (both absolute or relative file paths are accepted) + * @param filePath Path to file, absolute or relative to `rootUri` */ - fileExists(path: string): boolean { - return this.readFileIfExists(path) !== undefined || this.files.has(path) || this.files.has(util.path2uri(this.path, path)); + fileExists(filePath: string): boolean { + return this.readFileIfExists(filePath) !== undefined || this.files.has(filePath) || this.files.has(util.path2uri(this.rootUri, filePath).href); } /** @@ -131,17 +134,17 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti * @return file's content in the following order (overlay then cache). * If there is no such file, returns empty string to match expected signature */ - readFile(path: string): string { - return this.readFileIfExists(path) || ''; + readFile(filePath: string): string { + return this.readFileIfExists(filePath) || ''; } /** - * @param path file path (both absolute or relative file paths are accepted) + * @param filePath Path to the file, absolute or relative to `rootUri` * @return file's content in the following order (overlay then cache). * If there is no such file, returns undefined */ - private readFileIfExists(path: string): string | undefined { - const uri = util.path2uri(this.path, path); + readFileIfExists(filePath: string): string | undefined { + const uri = util.path2uri(this.rootUri, filePath).href; let content = this.overlay.get(uri); if (content !== undefined) { return content; @@ -155,7 +158,7 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti return content; } - return typeScriptLibraries.get(path); + return typeScriptLibraries.get(filePath); } /** diff --git a/src/project-manager.ts b/src/project-manager.ts index a8478966d..653789b07 100644 --- a/src/project-manager.ts +++ b/src/project-manager.ts @@ -91,7 +91,7 @@ export class ProjectManager implements Disposable { * @param strict indicates if we are working in strict mode (VFS) or with a local file system * @param traceModuleResolution allows to enable module resolution tracing (done by TS compiler) */ - constructor(rootPath: string, inMemoryFileSystem: InMemoryFileSystem, updater: FileSystemUpdater, strict: boolean, traceModuleResolution?: boolean, protected logger: Logger = new NoopLogger()) { + constructor(private rootUri: URL, rootPath: string, inMemoryFileSystem: InMemoryFileSystem, updater: FileSystemUpdater, strict: boolean, traceModuleResolution?: boolean, protected logger: Logger = new NoopLogger()) { this.rootPath = util.toUnixPath(rootPath); this.updater = updater; this.localFs = inMemoryFileSystem; @@ -496,7 +496,7 @@ export class ProjectManager implements Disposable { } const configType = this.getConfigurationType(filePath); const configs = this.configs[configType]; - configs.set(dir, new ProjectConfiguration(this.localFs, dir, this.versions, filePath, undefined, this.traceModuleResolution, this.logger)); + configs.set(dir, new ProjectConfiguration(this.rootUri, this.localFs, dir, this.versions, filePath, undefined, this.traceModuleResolution, this.logger)); } const rootPath = this.rootPath.replace(/\/+$/, ''); @@ -516,7 +516,7 @@ export class ProjectManager implements Disposable { if (configs.size > 0) { tsConfig.exclude = ['**/*']; } - configs.set(rootPath, new ProjectConfiguration(this.localFs, this.rootPath, this.versions, '', tsConfig, this.traceModuleResolution, this.logger)); + configs.set(rootPath, new ProjectConfiguration(this.rootUri, this.localFs, this.rootPath, this.versions, '', tsConfig, this.traceModuleResolution, this.logger)); } } @@ -591,7 +591,7 @@ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost { */ private versions: Map; - constructor(rootPath: string, options: ts.CompilerOptions, fs: InMemoryFileSystem, expectedFiles: string[], versions: Map, private logger: Logger = new NoopLogger()) { + constructor(private rootUri: URL, rootPath: string, options: ts.CompilerOptions, fs: InMemoryFileSystem, expectedFiles: string[], versions: Map, private logger: Logger = new NoopLogger()) { this.rootPath = rootPath; this.options = options; this.fs = fs; @@ -636,35 +636,31 @@ export class InMemoryLanguageServiceHost implements ts.LanguageServiceHost { } /** - * @param fileName relative or absolute file path + * @param fileName Path to the file, absolute or relative to `rootUri` */ - getScriptVersion(fileName: string): string { + getScriptVersion(filePath: string): string { - const uri = util.path2uri(this.rootPath, fileName); - if (path_.posix.isAbsolute(fileName) || path_.isAbsolute(fileName)) { - fileName = path_.posix.relative(this.rootPath, util.toUnixPath(fileName)); + const uri = util.path2uri(this.rootUri, filePath); + if (path_.posix.isAbsolute(filePath) || path_.isAbsolute(filePath)) { + filePath = path_.posix.relative(this.rootPath, util.toUnixPath(filePath)); } - let version = this.versions.get(uri); + let version = this.versions.get(uri.href); if (!version) { version = 1; - this.versions.set(uri, version); + this.versions.set(uri.href, version); } return '' + version; } /** - * @param fileName relative or absolute file path + * @param fileName Path to the file, absolute or relative to `rootUri` */ - getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined { - let exists = this.fs.fileExists(fileName); - if (!exists) { - fileName = path_.posix.join(this.rootPath, fileName); - exists = this.fs.fileExists(fileName); - } - if (!exists) { + getScriptSnapshot(filePath: string): ts.IScriptSnapshot | undefined { + const content = this.fs.readFileIfExists(filePath); + if (content === undefined) { return undefined; } - return ts.ScriptSnapshot.fromString(this.fs.readFile(fileName)); + return ts.ScriptSnapshot.fromString(content); } getCurrentDirectory(): string { @@ -755,7 +751,7 @@ export class ProjectConfiguration { * @param configFilePath configuration file path, absolute * @param configContent optional configuration content to use instead of reading configuration file) */ - constructor(fs: InMemoryFileSystem, rootFilePath: string, versions: Map, configFilePath: string, configContent?: any, traceModuleResolution?: boolean, private logger: Logger = new NoopLogger()) { + constructor(private rootUri: URL, fs: InMemoryFileSystem, rootFilePath: string, versions: Map, configFilePath: string, configContent?: any, traceModuleResolution?: boolean, private logger: Logger = new NoopLogger()) { this.fs = fs; this.configFilePath = configFilePath; this.configContent = configContent; @@ -891,6 +887,7 @@ export class ProjectConfiguration { options.traceResolution = true; } this.host = new InMemoryLanguageServiceHost( + this.rootUri, this.fs.path, options, this.fs, diff --git a/src/test/fs.test.ts b/src/test/fs.test.ts index 96d76bef1..0bdc13575 100644 --- a/src/test/fs.test.ts +++ b/src/test/fs.test.ts @@ -21,7 +21,7 @@ describe('fs.ts', () => { temporaryDir = await new Promise((resolve, reject) => { temp.mkdir('local-fs', (err: Error, dirPath: string) => err ? reject(err) : resolve(dirPath)); }); - baseUri = new URL(path2uri('', temporaryDir) + '/'); + baseUri = path2uri(new URL('file:///'), temporaryDir + '/'); await fs.mkdir(path.join(temporaryDir, 'foo')); await fs.mkdir(path.join(temporaryDir, '@types')); await fs.mkdir(path.join(temporaryDir, '@types', 'diff')); diff --git a/src/test/memfs.test.ts b/src/test/memfs.test.ts index f1c726895..863df7a15 100644 --- a/src/test/memfs.test.ts +++ b/src/test/memfs.test.ts @@ -3,6 +3,7 @@ import iterate from 'iterare'; import { InMemoryFileSystem, typeScriptLibraries } from '../memfs'; import { uri2path } from '../util'; import chaiAsPromised = require('chai-as-promised'); +import { URL } from 'whatwg-url'; chai.use(chaiAsPromised); const assert = chai.assert; @@ -10,19 +11,19 @@ describe('memfs.ts', () => { describe('InMemoryFileSystem', () => { describe('uris()', () => { it('should hide TypeScript library files', async () => { - const fs = new InMemoryFileSystem('/'); + const fs = new InMemoryFileSystem(new URL('file:///'), '/'); assert.isFalse(iterate(fs.uris()).some(uri => typeScriptLibraries.has(uri2path(uri)))); }); }); describe('fileExists()', () => { it('should expose TypeScript library files', async () => { - const fs = new InMemoryFileSystem('/'); + const fs = new InMemoryFileSystem(new URL('file:///'), '/'); assert.isTrue(iterate(typeScriptLibraries.keys()).every(path => fs.fileExists(path))); }); }); describe('readFile()', () => { it('should expose TypeScript library files', async () => { - const fs = new InMemoryFileSystem('/'); + const fs = new InMemoryFileSystem(new URL('file:///'), '/'); assert.isTrue(iterate(typeScriptLibraries.keys()).every(path => !!fs.readFile(path))); }); }); diff --git a/src/test/project-manager.test.ts b/src/test/project-manager.test.ts index 150fb7f10..2f8e4677d 100644 --- a/src/test/project-manager.test.ts +++ b/src/test/project-manager.test.ts @@ -15,7 +15,7 @@ describe('ProjectManager', () => { describe('getPackageName()', () => { beforeEach(async () => { - memfs = new InMemoryFileSystem('/'); + memfs = new InMemoryFileSystem(new URL('file:///'), '/'); const localfs = new MapFileSystem(new Map([ ['file:///package.json', '{"name": "package-name-1"}'], ['file:///subdirectory-with-tsconfig/package.json', '{"name": "package-name-2"}'], @@ -23,7 +23,7 @@ describe('ProjectManager', () => { ['file:///subdirectory-with-tsconfig/src/dummy.ts', ''] ])); const updater = new FileSystemUpdater(localfs, memfs); - projectManager = new ProjectManager('/', memfs, updater, true); + projectManager = new ProjectManager(new URL('file:///'), '/', memfs, updater, true); await projectManager.ensureAllFiles(); }); it('should resolve package name when package.json is at the same level', () => { @@ -35,7 +35,7 @@ describe('ProjectManager', () => { }); describe('ensureReferencedFiles()', () => { beforeEach(() => { - memfs = new InMemoryFileSystem('/'); + memfs = new InMemoryFileSystem(new URL('file:///'), '/'); const localfs = new MapFileSystem(new Map([ ['file:///package.json', '{"name": "package-name-1"}'], ['file:///node_modules/somelib/index.js', '/// \n/// '], @@ -44,7 +44,7 @@ describe('ProjectManager', () => { ['file:///src/dummy.ts', 'import * as somelib from "somelib";'] ])); const updater = new FileSystemUpdater(localfs, memfs); - projectManager = new ProjectManager('/', memfs, updater, true); + projectManager = new ProjectManager(new URL('file:///'), '/', memfs, updater, true); }); it('should ensure content for imports and references is fetched', async () => { await projectManager.ensureReferencedFiles(new URL('file:///src/dummy.ts')).toPromise(); @@ -55,13 +55,13 @@ describe('ProjectManager', () => { }); describe('getConfiguration()', () => { beforeEach(async () => { - memfs = new InMemoryFileSystem('/'); + memfs = new InMemoryFileSystem(new URL('file:///'), '/'); const localfs = new MapFileSystem(new Map([ ['file:///tsconfig.json', '{}'], ['file:///src/jsconfig.json', '{}'] ])); const updater = new FileSystemUpdater(localfs, memfs); - projectManager = new ProjectManager('/', memfs, updater, true); + projectManager = new ProjectManager(new URL('file:///'), '/', memfs, updater, true); await projectManager.ensureAllFiles(); }); it('should resolve best configuration based on file name', () => { diff --git a/src/typescript-service.ts b/src/typescript-service.ts index e2618ecbc..0221c7977 100644 --- a/src/typescript-service.ts +++ b/src/typescript-service.ts @@ -133,11 +133,11 @@ export class TypeScriptService { */ async initialize(params: InitializeParams, span = new Span()): Promise { if (params.rootUri || params.rootPath) { - this.rootUri = new URL(params.rootUri || util.path2uri('', params.rootPath!)); + this.rootUri = params.rootUri ? new URL(params.rootUri) : util.path2uri(new URL('file:'), params.rootPath!); this.root = params.rootPath || util.uri2path(this.rootUri); this._initializeFileSystems(!this.options.strict && !(params.capabilities.xcontentProvider && params.capabilities.xfilesProvider)); this.updater = new FileSystemUpdater(this.fileSystem, this.inMemoryFileSystem); - this.projectManager = new pm.ProjectManager(this.root, this.inMemoryFileSystem, this.updater, !!this.options.strict, this.traceModuleResolution, this.logger); + this.projectManager = new pm.ProjectManager(this.rootUri, this.root, this.inMemoryFileSystem, this.updater, !!this.options.strict, this.traceModuleResolution, this.logger); // Detect DefinitelyTyped this.isDefinitelyTyped = (async () => { try { @@ -194,7 +194,7 @@ export class TypeScriptService { */ protected _initializeFileSystems(accessDisk: boolean): void { this.fileSystem = accessDisk ? new LocalFileSystem(this.rootUri) : new RemoteFileSystem(this.client); - this.inMemoryFileSystem = new InMemoryFileSystem(this.root); + this.inMemoryFileSystem = new InMemoryFileSystem(this.rootUri, this.root); } /** @@ -239,7 +239,7 @@ export class TypeScriptService { } const start = ts.getLineAndCharacterOfPosition(sourceFile, def.textSpan.start); const end = ts.getLineAndCharacterOfPosition(sourceFile, def.textSpan.start + def.textSpan.length); - ret.push(Location.create(this._defUri(def.fileName), { + ret.push(Location.create(this._defUri(def.fileName).href, { start, end })); @@ -289,7 +289,7 @@ export class TypeScriptService { return { symbol: util.defInfoToSymbolDescriptor(def), location: { - uri: this._defUri(def.fileName), + uri: this._defUri(def.fileName).href, range: { start, end @@ -386,7 +386,7 @@ export class TypeScriptService { const start = ts.getLineAndCharacterOfPosition(sourceFile, reference.textSpan.start); const end = ts.getLineAndCharacterOfPosition(sourceFile, reference.textSpan.start + reference.textSpan.length); return { - uri: util.path2uri(this.root, reference.fileName), + uri: util.path2uri(this.rootUri, reference.fileName).href, range: { start, end @@ -569,7 +569,7 @@ export class TypeScriptService { .map(symbol => ({ symbol, reference: { - uri: this._defUri(source.fileName), + uri: this._defUri(source.fileName).href, range: { start: ts.getLineAndCharacterOfPosition(source, node.pos + 1), end: ts.getLineAndCharacterOfPosition(source, node.end) @@ -908,7 +908,7 @@ export class TypeScriptService { name: item.name, kind: util.convertStringtoSymbolKind(item.kind), location: { - uri: this._defUri(item.fileName), + uri: this._defUri(item.fileName).href, range: { start: ts.getLineAndCharacterOfPosition(sourceFile, item.textSpan.start), end: ts.getLineAndCharacterOfPosition(sourceFile, item.textSpan.start + item.textSpan.length) @@ -932,11 +932,11 @@ export class TypeScriptService { * Transforms definition's file name to URI. If definition belongs to TypeScript library, * returns git://github.com/Microsoft/TypeScript URL, otherwise returns file:// one */ - private _defUri(filePath: string): string { + private _defUri(filePath: string): URL { if (isTypeScriptLibrary(filePath)) { - return 'git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/' + path_.basename(filePath); + return new URL('git://github.com/Microsoft/TypeScript?v' + ts.version + '#lib/' + path_.basename(filePath)); } - return util.path2uri(this.root, filePath); + return util.path2uri(this.rootUri, filePath); } /** @@ -974,7 +974,7 @@ export class TypeScriptService { name: item.text, kind: util.convertStringtoSymbolKind(item.kind), location: { - uri: this._defUri(sourceFile.fileName), + uri: this._defUri(sourceFile.fileName).href, range: { start: ts.getLineAndCharacterOfPosition(sourceFile, span.start), end: ts.getLineAndCharacterOfPosition(sourceFile, span.start + span.length) diff --git a/src/util.ts b/src/util.ts index 9e2de24b6..ecd0ccb4d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,8 +1,8 @@ import * as os from 'os'; import * as path from 'path'; - import * as ts from 'typescript'; import { Position, Range, SymbolKind } from 'vscode-languageserver'; +import { URL } from 'whatwg-url'; import * as rt from './request-type'; let strict = false; @@ -79,19 +79,18 @@ export function convertStringtoSymbolKind(kind: string): SymbolKind { } } -export function path2uri(root: string, file: string): string { - let ret = 'file://'; - if (!strict && process.platform === 'win32') { - ret += '/'; - } - let p; - if (root) { - p = resolve(root, file); - } else { - p = file; +/** + * Returns the given file path as a URL. + * The returned URL uses protocol and host of the passed root URL + */ +export function path2uri(root: URL, file: string): URL { + const uri = new URL(root.href); + let pathname = file.split(/[\\\/]/).map((val, i) => i <= 1 && /^[a-z]:$/i.test(val) ? val : encodeURIComponent(val)).join('/'); + if (!pathname.startsWith('/')) { + pathname = '/' + pathname; } - p = toUnixPath(p).split('/').map((val, i) => i <= 1 && /^[a-z]:$/i.test(val) ? val : encodeURIComponent(val)).join('/'); - return ret + p; + uri.pathname = pathname; + return uri; } /** From cb22fdd4295678b5746f1eba2d2a323b8bf90002 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 27 Apr 2017 18:36:15 +0200 Subject: [PATCH 04/11] Reintroduce resolveUriToPath() --- src/fs.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/fs.ts b/src/fs.ts index 3941a25e4..5a42d3be5 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -56,13 +56,20 @@ export class LocalFileSystem implements FileSystem { /** * @param rootUri The workspace root URI that is used when no base is given */ - constructor(private rootUri: URL) {} + constructor(protected rootUri: URL) {} + + /** + * Returns the file path where a given URI should be located on disk + */ + protected resolveUriToPath(uri: URL): string { + return uri2path(uri); + } async getWorkspaceFiles(base: URL = this.rootUri): Promise> { const files = await new Promise((resolve, reject) => { glob('*', { // Search the base directory - cwd: uri2path(base), + cwd: this.resolveUriToPath(base), // Don't return directories nodir: true, // Search directories recursively @@ -75,7 +82,7 @@ export class LocalFileSystem implements FileSystem { } async getTextDocumentContent(uri: URL): Promise { - return fs.readFile(uri2path(uri), 'utf8'); + return fs.readFile(this.resolveUriToPath(uri), 'utf8'); } } From 68454555a5587c077772e189ff4d128a93ddca0b Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 27 Apr 2017 23:21:44 +0200 Subject: [PATCH 05/11] Make ensureReferencedFiles() return URLs --- src/memfs.ts | 6 +++--- src/project-manager.ts | 12 ++++++------ src/test/util-test.ts | 27 +++++++++++++++++++++++++-- src/typescript-service.ts | 16 ++++++++-------- src/util.ts | 14 ++++---------- 5 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/memfs.ts b/src/memfs.ts index af4ad19cc..c9e00274c 100644 --- a/src/memfs.ts +++ b/src/memfs.ts @@ -74,7 +74,7 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti this.files.set(uri.href, content); } // Add to directory tree - const filePath = util.uri2path(uri); + const filePath = util.toUnixPath(util.uri2path(uri)); const components = filePath.split('/').filter(c => c); let node = this.rootNode; for (const [i, component] of components.entries()) { @@ -99,7 +99,7 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti * @param uri URI to a file */ has(uri: URL): boolean { - return this.files.has(uri.href) || this.fileExists(util.uri2path(uri)); + return this.files.has(uri.href) || this.fileExists(util.toUnixPath(util.uri2path(uri))); } /** @@ -112,7 +112,7 @@ export class InMemoryFileSystem implements ts.ParseConfigHost, ts.ModuleResoluti getContent(uri: URL): string { let content = this.files.get(uri.href); if (content === undefined) { - content = typeScriptLibraries.get(util.uri2path(uri)); + content = typeScriptLibraries.get(util.toUnixPath(util.uri2path(uri))); } if (content === undefined) { throw new Error(`Content of ${uri} is not available in memory`); diff --git a/src/project-manager.ts b/src/project-manager.ts index 653789b07..fe7dd66c7 100644 --- a/src/project-manager.ts +++ b/src/project-manager.ts @@ -265,7 +265,7 @@ export class ProjectManager implements Disposable { * @param childOf OpenTracing parent span for tracing * @return Observable of file URIs ensured */ - ensureReferencedFiles(uri: URL, maxDepth = 30, ignore = new Set(), childOf = new Span()): Observable { + ensureReferencedFiles(uri: URL, maxDepth = 30, ignore = new Set(), childOf = new Span()): Observable { const span = childOf.tracer().startSpan('Ensure referenced files', { childOf }); span.addTags({ uri, maxDepth }); ignore.add(uri.href); @@ -284,7 +284,7 @@ export class ProjectManager implements Disposable { }) ) // Log errors to span - .catch(err => { + .catch(err => { span.setTag('error', true); span.log({ 'event': 'error', 'error.object': err, 'message': err.message, 'stack': err.stack }); throw err; @@ -433,7 +433,7 @@ export class ProjectManager implements Disposable { * @param uri file's URI */ didClose(uri: URL, span = new Span()) { - const filePath = util.uri2path(uri); + const filePath = util.toUnixPath(util.uri2path(uri)); this.localFs.didClose(uri); let version = this.versions.get(uri.href) || 0; this.versions.set(uri.href, ++version); @@ -452,7 +452,7 @@ export class ProjectManager implements Disposable { * @param text file's content */ didChange(uri: URL, text: string, span = new Span()) { - const filePath = util.uri2path(uri); + const filePath = util.toUnixPath(util.uri2path(uri)); this.localFs.didChange(uri, text); let version = this.versions.get(uri.href) || 0; this.versions.set(uri.href, ++version); @@ -480,7 +480,7 @@ export class ProjectManager implements Disposable { */ createConfigurations() { for (const uri of this.localFs.uris()) { - const filePath = util.uri2path(uri); + const filePath = util.toUnixPath(util.uri2path(uri)); if (!/(^|\/)[tj]sconfig\.json$/.test(filePath)) { continue; } @@ -926,7 +926,7 @@ export class ProjectConfiguration { let changed = false; for (const uri of this.fs.uris()) { - const fileName = util.uri2path(uri); + const fileName = util.toUnixPath(util.uri2path(uri)); if (util.isGlobalTSFile(fileName) || (!util.isDependencyFile(fileName) && util.isDeclarationFile(fileName))) { const sourceFile = program.getSourceFile(fileName); if (!sourceFile) { diff --git a/src/test/util-test.ts b/src/test/util-test.ts index fbbfeac8c..acb456844 100644 --- a/src/test/util-test.ts +++ b/src/test/util-test.ts @@ -1,6 +1,29 @@ -import { symbolDescriptorMatch } from '../util'; +import * as assert from 'assert'; +import { URL } from 'whatwg-url'; +import { path2uri, symbolDescriptorMatch, uri2path } from '../util'; -describe('util tests', () => { +describe('util', () => { + describe('path2uri()', () => { + it('should convert a Unix file path to a URI', () => { + const uri = path2uri(new URL('file://host/foo/bar'), '/baz/@qux'); + assert.equal(uri.href, 'file://host/baz/%40qux'); + }); + it('should convert a Windows file path to a URI', () => { + // Host is dropped because of https://github.com/jsdom/whatwg-url/issues/84 + const uri = path2uri(new URL('file:///foo/bar'), 'C:\\baz\\@qux'); + assert.equal(uri.href, 'file:///C:/baz/%40qux'); + }); + }); + describe('uri2path()', () => { + it('should convert a Unix file URI to a file path', () => { + const filePath = uri2path(new URL('file:///baz/%40qux')); + assert.equal(filePath, '/baz/@qux'); + }); + it('should convert a Windows file URI to a file path', () => { + const filePath = uri2path(new URL('file:///c:/baz/%40qux')); + assert.equal(filePath, 'c:\\baz\\@qux'); + }); + }); describe('symbolDescriptorMatch', () => { it('', (done: (err?: Error) => void) => { const want = true; diff --git a/src/typescript-service.ts b/src/typescript-service.ts index 0221c7977..8155cbf39 100644 --- a/src/typescript-service.ts +++ b/src/typescript-service.ts @@ -219,7 +219,7 @@ export class TypeScriptService { // Fetch files needed to resolve definition await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const fileName: string = util.uri2path(uri); + const fileName: string = util.toUnixPath(util.uri2path(uri)); const configuration = this.projectManager.getConfiguration(fileName); configuration.ensureBasicFiles(span); @@ -265,7 +265,7 @@ export class TypeScriptService { .toArray() .mergeMap(() => { // Convert URI to file path - const fileName: string = util.uri2path(uri); + const fileName: string = util.toUnixPath(util.uri2path(uri)); // Get closest tsconfig configuration const configuration = this.projectManager.getConfiguration(fileName); configuration.ensureBasicFiles(span); @@ -312,7 +312,7 @@ export class TypeScriptService { // Ensure files needed to resolve hover are fetched await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const fileName: string = util.uri2path(uri); + const fileName: string = util.toUnixPath(util.uri2path(uri)); const configuration = this.projectManager.getConfiguration(fileName); configuration.ensureBasicFiles(span); @@ -351,7 +351,7 @@ export class TypeScriptService { return Observable.from(this.projectManager.ensureOwnFiles(span)) .mergeMap(() => { // Convert URI to file path because TypeScript doesn't work with URIs - const fileName = util.uri2path(uri); + const fileName = util.toUnixPath(util.uri2path(uri)); // Get tsconfig configuration for requested file const configuration = this.projectManager.getConfiguration(fileName); // Ensure all files have been added @@ -504,7 +504,7 @@ export class TypeScriptService { // Search symbol in configuration // forcing TypeScript mode - const config = this.projectManager.getConfiguration(util.uri2path(packageRootUri), 'ts'); + const config = this.projectManager.getConfiguration(util.toUnixPath(util.uri2path(packageRootUri)), 'ts'); return Array.from(this._collectWorkspaceSymbols(config, params.query || symbolQuery, params.limit)); } catch (err) { span.setTag('error', true); @@ -525,7 +525,7 @@ export class TypeScriptService { // Ensure files needed to resolve symbols are fetched await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const fileName = util.uri2path(uri); + const fileName = util.toUnixPath(util.uri2path(uri)); const config = this.projectManager.getConfiguration(fileName); config.ensureBasicFiles(span); @@ -698,7 +698,7 @@ export class TypeScriptService { // Ensure files needed to suggest completions are fetched await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const fileName: string = util.uri2path(uri); + const fileName: string = util.toUnixPath(util.uri2path(uri)); const configuration = this.projectManager.getConfiguration(fileName); configuration.ensureBasicFiles(span); @@ -743,7 +743,7 @@ export class TypeScriptService { // Ensure files needed to resolve signature are fetched await this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span).toPromise(); - const filePath = util.uri2path(uri); + const filePath = util.toUnixPath(util.uri2path(uri)); const configuration = this.projectManager.getConfiguration(filePath); configuration.ensureBasicFiles(span); diff --git a/src/util.ts b/src/util.ts index ecd0ccb4d..de2fbbef0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -100,19 +100,13 @@ export function path2uri(root: URL, file: string): URL { */ export function uri2path(uri: URL): string { // %-decode parts - const parts = uri.pathname.split('/').map(part => { - try { - return decodeURIComponent(part); - } catch (err) { - throw new Error(`Error decoding ${part} of ${uri}: ${err.message}`); - } - }); + let filePath = decodeURIComponent(uri.pathname); // Strip the leading slash on Windows - const isWindowsUri = parts[0] && /^\/[a-z]:\//.test(parts[0]); + const isWindowsUri = /^\/[a-z]:\//i.test(filePath); if (isWindowsUri) { - parts[0] = parts[0].substr(1); + filePath = filePath.substr(1); } - return parts.join(isWindowsUri ? '\\' : '/'); + return filePath.replace(/\//g, isWindowsUri ? '\\' : '/'); } export function isLocalUri(uri: string): boolean { From 63d9cd942d4387443eaadb8c40e88338bd92f851 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 28 Apr 2017 15:55:44 +0200 Subject: [PATCH 06/11] Let URL handle encoding --- src/fs.ts | 2 +- src/test/fs.test.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/fs.ts b/src/fs.ts index 5a42d3be5..eefde002f 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -78,7 +78,7 @@ export class LocalFileSystem implements FileSystem { absolute: true } as any, (err, matches) => err ? reject(err) : resolve(matches)); }); - return iterate(files).map(file => new URL(file.split('/').map(encodeURIComponent).join('/'), base.href + '/')); + return iterate(files).map(file => new URL(file, base.href + '/')); } async getTextDocumentContent(uri: URL): Promise { diff --git a/src/test/fs.test.ts b/src/test/fs.test.ts index 0bdc13575..cb411979a 100644 --- a/src/test/fs.test.ts +++ b/src/test/fs.test.ts @@ -22,12 +22,12 @@ describe('fs.ts', () => { temp.mkdir('local-fs', (err: Error, dirPath: string) => err ? reject(err) : resolve(dirPath)); }); baseUri = path2uri(new URL('file:///'), temporaryDir + '/'); - await fs.mkdir(path.join(temporaryDir, 'foo')); + await fs.mkdir(path.join(temporaryDir, 'f💩o')); await fs.mkdir(path.join(temporaryDir, '@types')); await fs.mkdir(path.join(temporaryDir, '@types', 'diff')); await fs.writeFile(path.join(temporaryDir, 'tweedledee'), 'hi'); await fs.writeFile(path.join(temporaryDir, 'tweedledum'), 'bye'); - await fs.writeFile(path.join(temporaryDir, 'foo', 'bar.ts'), 'baz'); + await fs.writeFile(path.join(temporaryDir, 'f💩o', 'bar.ts'), 'baz'); await fs.writeFile(path.join(temporaryDir, '@types', 'diff', 'index.d.ts'), 'baz'); fileSystem = new LocalFileSystem(baseUri); }); @@ -43,14 +43,14 @@ describe('fs.ts', () => { assert.sameMembers(files, [ baseUri + 'tweedledee', baseUri + 'tweedledum', - baseUri + 'foo/bar.ts', - baseUri + '%40types/diff/index.d.ts' + baseUri + 'f%F0%9F%92%A9o/bar.ts', + baseUri + '@types/diff/index.d.ts' ]); }); it('should return all files under specific root', async () => { - const files = iterate(await fileSystem.getWorkspaceFiles(new URL('foo/', baseUri.href))).map(uri => uri.href).toArray(); + const files = iterate(await fileSystem.getWorkspaceFiles(new URL('f%F0%9F%92%A9o/', baseUri.href))).map(uri => uri.href).toArray(); assert.sameMembers(files, [ - baseUri + 'foo/bar.ts' + baseUri + 'f%F0%9F%92%A9o/bar.ts' ]); }); }); From 91a785488d5e737042e6a6c448a6f2ad50b27dd3 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 28 Apr 2017 16:19:57 +0200 Subject: [PATCH 07/11] Use path2uri() --- src/project-manager.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/project-manager.ts b/src/project-manager.ts index fe7dd66c7..f732e8f91 100644 --- a/src/project-manager.ts +++ b/src/project-manager.ts @@ -5,7 +5,6 @@ import * as os from 'os'; import * as path_ from 'path'; import * as ts from 'typescript'; import { Disposable } from 'vscode-languageserver'; -import { URL } from 'whatwg-url'; import { FileSystemUpdater } from './fs'; import { Logger, NoopLogger } from './logging'; import { InMemoryFileSystem } from './memfs'; @@ -320,10 +319,10 @@ export class ProjectManager implements Disposable { if (observable) { return observable; } - // TypeScript works with file paths, not URIs - const filePath = uri.pathname.split('/').map(decodeURIComponent).join('/'); observable = Observable.from(this.updater.ensure(uri)) .mergeMap(() => { + // TypeScript works with file paths, not URIs + const filePath = util.uri2path(uri); const config = this.getConfiguration(filePath); config.ensureBasicFiles(span); const contents = this.localFs.getContent(uri); @@ -366,7 +365,7 @@ export class ProjectManager implements Disposable { ); }) // Use same scheme, slashes, host for referenced URI as input file - .map(filePath => new URL(filePath.split(/[\\\/]/).map(encodeURIComponent).join('/'), uri.href)) + .map(filePath => util.path2uri(uri, filePath)) // Don't cache errors .catch(err => { this.referencedFiles.delete(uri.href); @@ -527,6 +526,7 @@ export class ProjectManager implements Disposable { * @return configuration type to use for a given file */ private getConfigurationType(filePath: string): ConfigType { + filePath = util.toUnixPath(filePath); const name = path_.posix.basename(filePath); if (name === 'tsconfig.json') { return 'ts'; From fd2b21285144ea1cb76d896c37c340e62fa78556 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Fri, 28 Apr 2017 16:22:48 +0200 Subject: [PATCH 08/11] Compare drive letters case-insensitively --- src/test/util-test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/util-test.ts b/src/test/util-test.ts index acb456844..234a385b5 100644 --- a/src/test/util-test.ts +++ b/src/test/util-test.ts @@ -23,6 +23,10 @@ describe('util', () => { const filePath = uri2path(new URL('file:///c:/baz/%40qux')); assert.equal(filePath, 'c:\\baz\\@qux'); }); + it('should convert a Windows file URI with uppercase drive letter to a file path', () => { + const filePath = uri2path(new URL('file:///C:/baz/%40qux')); + assert.equal(filePath, 'C:\\baz\\@qux'); + }); }); describe('symbolDescriptorMatch', () => { it('', (done: (err?: Error) => void) => { From 30267609c0eebd2f69b03de5cfc8f78f135053fc Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sat, 29 Apr 2017 12:08:55 +0200 Subject: [PATCH 09/11] Use path2uri in fs --- src/fs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fs.ts b/src/fs.ts index eefde002f..8da8afa94 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -6,7 +6,7 @@ import { Span } from 'opentracing'; import Semaphore from 'semaphore-async-await'; import { URL } from 'whatwg-url'; import { InMemoryFileSystem } from './memfs'; -import { uri2path } from './util'; +import { path2uri, uri2path } from './util'; export interface FileSystem { /** @@ -78,7 +78,7 @@ export class LocalFileSystem implements FileSystem { absolute: true } as any, (err, matches) => err ? reject(err) : resolve(matches)); }); - return iterate(files).map(file => new URL(file, base.href + '/')); + return iterate(files).map(filePath => path2uri(base, filePath)); } async getTextDocumentContent(uri: URL): Promise { From 87af88110f1e8f532d4b992b54ce8ef8acd98a42 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sat, 29 Apr 2017 13:31:33 +0200 Subject: [PATCH 10/11] use path.sep --- src/test/fs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/fs.test.ts b/src/test/fs.test.ts index cb411979a..e7da31f8e 100644 --- a/src/test/fs.test.ts +++ b/src/test/fs.test.ts @@ -21,7 +21,7 @@ describe('fs.ts', () => { temporaryDir = await new Promise((resolve, reject) => { temp.mkdir('local-fs', (err: Error, dirPath: string) => err ? reject(err) : resolve(dirPath)); }); - baseUri = path2uri(new URL('file:///'), temporaryDir + '/'); + baseUri = path2uri(new URL('file:///'), temporaryDir + path.sep); await fs.mkdir(path.join(temporaryDir, 'f💩o')); await fs.mkdir(path.join(temporaryDir, '@types')); await fs.mkdir(path.join(temporaryDir, '@types', 'diff')); From d8f9e44a2f4f579d9d55d26899f0c5665a6254db Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Sat, 29 Apr 2017 13:48:35 +0200 Subject: [PATCH 11/11] Add more tests for special chars --- src/test/fs.test.ts | 16 +++++++++------- src/test/util-test.ts | 37 ++++++++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/test/fs.test.ts b/src/test/fs.test.ts index e7da31f8e..a60259af3 100644 --- a/src/test/fs.test.ts +++ b/src/test/fs.test.ts @@ -22,12 +22,13 @@ describe('fs.ts', () => { temp.mkdir('local-fs', (err: Error, dirPath: string) => err ? reject(err) : resolve(dirPath)); }); baseUri = path2uri(new URL('file:///'), temporaryDir + path.sep); - await fs.mkdir(path.join(temporaryDir, 'f💩o')); + await fs.mkdir(path.join(temporaryDir, 'foo')); await fs.mkdir(path.join(temporaryDir, '@types')); await fs.mkdir(path.join(temporaryDir, '@types', 'diff')); + await fs.writeFile(path.join(temporaryDir, '💩'), 'hi'); await fs.writeFile(path.join(temporaryDir, 'tweedledee'), 'hi'); await fs.writeFile(path.join(temporaryDir, 'tweedledum'), 'bye'); - await fs.writeFile(path.join(temporaryDir, 'f💩o', 'bar.ts'), 'baz'); + await fs.writeFile(path.join(temporaryDir, 'foo', 'bar.ts'), 'baz'); await fs.writeFile(path.join(temporaryDir, '@types', 'diff', 'index.d.ts'), 'baz'); fileSystem = new LocalFileSystem(baseUri); }); @@ -43,19 +44,20 @@ describe('fs.ts', () => { assert.sameMembers(files, [ baseUri + 'tweedledee', baseUri + 'tweedledum', - baseUri + 'f%F0%9F%92%A9o/bar.ts', - baseUri + '@types/diff/index.d.ts' + baseUri + '%F0%9F%92%A9', + baseUri + 'foo/bar.ts', + baseUri + '%40types/diff/index.d.ts' ]); }); it('should return all files under specific root', async () => { - const files = iterate(await fileSystem.getWorkspaceFiles(new URL('f%F0%9F%92%A9o/', baseUri.href))).map(uri => uri.href).toArray(); + const files = iterate(await fileSystem.getWorkspaceFiles(new URL('foo/', baseUri.href))).map(uri => uri.href).toArray(); assert.sameMembers(files, [ - baseUri + 'f%F0%9F%92%A9o/bar.ts' + baseUri + 'foo/bar.ts' ]); }); }); describe('getTextDocumentContent()', () => { - it('should read files denoted by absolute URI', async () => { + it('should return the content of a file by URI', async () => { assert.equal(await fileSystem.getTextDocumentContent(new URL('tweedledee', baseUri.href)), 'hi'); }); }); diff --git a/src/test/util-test.ts b/src/test/util-test.ts index 234a385b5..3184dc7f0 100644 --- a/src/test/util-test.ts +++ b/src/test/util-test.ts @@ -5,27 +5,42 @@ import { path2uri, symbolDescriptorMatch, uri2path } from '../util'; describe('util', () => { describe('path2uri()', () => { it('should convert a Unix file path to a URI', () => { - const uri = path2uri(new URL('file://host/foo/bar'), '/baz/@qux'); - assert.equal(uri.href, 'file://host/baz/%40qux'); + const uri = path2uri(new URL('file:///foo/bar'), '/baz/qux'); + assert.equal(uri.href, 'file:///baz/qux'); }); it('should convert a Windows file path to a URI', () => { - // Host is dropped because of https://github.com/jsdom/whatwg-url/issues/84 - const uri = path2uri(new URL('file:///foo/bar'), 'C:\\baz\\@qux'); - assert.equal(uri.href, 'file:///C:/baz/%40qux'); + const uri = path2uri(new URL('file:///foo/bar'), 'C:\\baz\\qux'); + assert.equal(uri.href, 'file:///C:/baz/qux'); + }); + it('should encode special characters', () => { + const uri = path2uri(new URL('file:///foo/bar'), '/💩'); + assert.equal(uri.href, 'file:///%F0%9F%92%A9'); + }); + it('should encode unreserved special characters', () => { + const uri = path2uri(new URL('file:///foo/bar'), '/@baz'); + assert.equal(uri.href, 'file:///%40baz'); }); }); describe('uri2path()', () => { it('should convert a Unix file URI to a file path', () => { - const filePath = uri2path(new URL('file:///baz/%40qux')); - assert.equal(filePath, '/baz/@qux'); + const filePath = uri2path(new URL('file:///baz/qux')); + assert.equal(filePath, '/baz/qux'); }); it('should convert a Windows file URI to a file path', () => { - const filePath = uri2path(new URL('file:///c:/baz/%40qux')); - assert.equal(filePath, 'c:\\baz\\@qux'); + const filePath = uri2path(new URL('file:///c:/baz/qux')); + assert.equal(filePath, 'c:\\baz\\qux'); }); it('should convert a Windows file URI with uppercase drive letter to a file path', () => { - const filePath = uri2path(new URL('file:///C:/baz/%40qux')); - assert.equal(filePath, 'C:\\baz\\@qux'); + const filePath = uri2path(new URL('file:///C:/baz/qux')); + assert.equal(filePath, 'C:\\baz\\qux'); + }); + it('should decode special characters', () => { + const filePath = uri2path(new URL('file:///%F0%9F%92%A9')); + assert.equal(filePath, '/💩'); + }); + it('should decode unreserved special characters', () => { + const filePath = uri2path(new URL('file:///%40foo')); + assert.equal(filePath, '/@foo'); }); }); describe('symbolDescriptorMatch', () => {