From d1dcab4787504b14dc17ec7825a39e75fb8970b5 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 4 May 2017 22:15:12 +0200 Subject: [PATCH 1/3] Normalize URI encodings --- src/fs.ts | 6 +++--- src/typescript-service.ts | 22 +++++++++++----------- src/util.ts | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/fs.ts b/src/fs.ts index 729817c61..2493bfe3e 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -6,7 +6,7 @@ import iterate from 'iterare'; import { Span } from 'opentracing'; import Semaphore from 'semaphore-async-await'; import { InMemoryFileSystem } from './memfs'; -import { normalizeDir, path2uri, toUnixPath, uri2path } from './util'; +import { normalizeDir, normalizeUri, path2uri, toUnixPath, uri2path } from './util'; export interface FileSystem { /** @@ -36,7 +36,7 @@ export class RemoteFileSystem implements FileSystem { */ async getWorkspaceFiles(base?: string, childOf = new Span()): Promise> { return iterate(await this.client.workspaceXfiles({ base }, childOf)) - .map(textDocument => textDocument.uri); + .map(textDocument => normalizeUri(textDocument.uri)); } /** @@ -69,7 +69,7 @@ export class LocalFileSystem implements FileSystem { const files = await new Promise((resolve, reject) => { glob('*', { cwd: root, nodir: true, matchBase: true }, (err, matches) => err ? reject(err) : resolve(matches)); }); - return iterate(files).map(file => baseUri + file.split('/').map(encodeURIComponent).join('/')); + return iterate(files).map(file => normalizeUri(baseUri + file)); } async getTextDocumentContent(uri: string): Promise { diff --git a/src/typescript-service.ts b/src/typescript-service.ts index 0c393bc28..995a95e94 100644 --- a/src/typescript-service.ts +++ b/src/typescript-service.ts @@ -228,7 +228,7 @@ export class TypeScriptService { * location of a symbol at a given text document position. */ textDocumentDefinition(params: TextDocumentPositionParams, span = new Span()): Observable { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Fetch files needed to resolve definition return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) @@ -277,7 +277,7 @@ export class TypeScriptService { * know some information about it. */ textDocumentXdefinition(params: TextDocumentPositionParams, span = new Span()): Observable { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure files needed to resolve SymbolLocationInformation are fetched return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) @@ -326,7 +326,7 @@ export class TypeScriptService { * given text document position. */ textDocumentHover(params: TextDocumentPositionParams, span = new Span()): Observable { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure files needed to resolve hover are fetched return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) @@ -373,7 +373,7 @@ 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 = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure all files were fetched to collect all references return Observable.from(this.projectManager.ensureOwnFiles(span)) .mergeMap(() => { @@ -551,7 +551,7 @@ export class TypeScriptService { * in a given text document. */ textDocumentDocumentSymbol(params: DocumentSymbolParams, span = new Span()): Observable { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure files needed to resolve symbols are fetched return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) @@ -742,7 +742,7 @@ export class TypeScriptService { * property filled in. */ textDocumentCompletion(params: TextDocumentPositionParams, span = new Span()): Observable { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure files needed to suggest completions are fetched return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) @@ -793,7 +793,7 @@ export class TypeScriptService { * information at a given cursor position. */ textDocumentSignatureHelp(params: TextDocumentPositionParams, span = new Span()): Observable { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure files needed to resolve signature are fetched return this.projectManager.ensureReferencedFiles(uri, undefined, undefined, span) @@ -844,7 +844,7 @@ export class TypeScriptService { * to read the document's truth using the document's uri. */ async textDocumentDidOpen(params: DidOpenTextDocumentParams): Promise { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure files needed for most operations are fetched await this.projectManager.ensureReferencedFiles(uri).toPromise(); this.projectManager.didOpen(uri, params.textDocument.text); @@ -857,7 +857,7 @@ export class TypeScriptService { * and language ids. */ async textDocumentDidChange(params: DidChangeTextDocumentParams): Promise { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); let text: string | undefined; for (const change of params.contentChanges) { if (change.range || change.rangeLength) { @@ -900,7 +900,7 @@ export class TypeScriptService { * saved in the client. */ async textDocumentDidSave(params: DidSaveTextDocumentParams): Promise { - const uri = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure files needed to suggest completions are fetched await this.projectManager.ensureReferencedFiles(uri).toPromise(); @@ -913,7 +913,7 @@ 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 = params.textDocument.uri; + const uri = util.normalizeUri(params.textDocument.uri); // Ensure files needed to suggest completions are fetched await this.projectManager.ensureReferencedFiles(uri).toPromise(); diff --git a/src/util.ts b/src/util.ts index 54097bfb8..df128ee09 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,6 +1,7 @@ import * as os from 'os'; import * as path from 'path'; import * as ts from 'typescript'; +import * as url from 'url'; import { Position, Range, SymbolKind } from 'vscode-languageserver'; import * as rt from './request-type'; @@ -78,6 +79,23 @@ export function convertStringtoSymbolKind(kind: string): SymbolKind { } } +/** + * Normalizes URI encoding by encoding _all_ special characters in the pathname + */ +export function normalizeUri(uri: string): string { + const parts = url.parse(uri); + if (!parts.pathname) { + return uri; + } + const pathParts = parts.pathname.split('/').map(segment => encodeURIComponent(decodeURIComponent(segment))); + // Decode Windows drive letter colon + if (/^[a-z]%3A$/i.test(pathParts[1])) { + pathParts[1] = decodeURIComponent(pathParts[1]); + } + parts.pathname = pathParts.join('/'); + return url.format(parts); +} + export function path2uri(root: string, file: string): string { let ret = 'file://'; if (!strict && process.platform === 'win32') { From d9009e742a3bcfad9c24b784de5ca65f7f60ad7c Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 4 May 2017 22:28:10 +0200 Subject: [PATCH 2/3] Use it in path2uri --- src/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.ts b/src/util.ts index df128ee09..33a073969 100644 --- a/src/util.ts +++ b/src/util.ts @@ -108,7 +108,7 @@ export function path2uri(root: string, file: string): string { p = file; } p = toUnixPath(p).split('/').map((val, i) => i <= 1 && /^[a-z]:$/i.test(val) ? val : encodeURIComponent(val)).join('/'); - return ret + p; + return normalizeUri(ret + p); } export function uri2path(uri: string): string { From 174d8399bc7fbdd79c580828c3a3b3036b8364b1 Mon Sep 17 00:00:00 2001 From: Felix Becker Date: Thu, 4 May 2017 23:04:29 +0200 Subject: [PATCH 3/3] Fix tests on Windows --- src/fs.ts | 4 ++-- src/test/fs.test.ts | 3 --- src/util.ts | 13 ++++++++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/fs.ts b/src/fs.ts index 2493bfe3e..0b8f260f4 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -6,7 +6,7 @@ import iterate from 'iterare'; import { Span } from 'opentracing'; import Semaphore from 'semaphore-async-await'; import { InMemoryFileSystem } from './memfs'; -import { normalizeDir, normalizeUri, path2uri, toUnixPath, uri2path } from './util'; +import { normalizeDir, normalizeUri, path2uri, uriToLocalPath } from './util'; export interface FileSystem { /** @@ -59,7 +59,7 @@ export class LocalFileSystem implements FileSystem { * Converts the URI to an absolute path */ protected resolveUriToPath(uri: string): string { - return toUnixPath(path.resolve(this.rootPath, uri2path(uri))); + return path.resolve(this.rootPath, uriToLocalPath(uri)); } async getWorkspaceFiles(base?: string): Promise> { diff --git a/src/test/fs.test.ts b/src/test/fs.test.ts index b43bc57db..b0d3f1f71 100644 --- a/src/test/fs.test.ts +++ b/src/test/fs.test.ts @@ -52,9 +52,6 @@ describe('fs.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'); }); diff --git a/src/util.ts b/src/util.ts index 33a073969..9b9830138 100644 --- a/src/util.ts +++ b/src/util.ts @@ -107,7 +107,10 @@ export function path2uri(root: string, file: string): string { } else { p = file; } - p = toUnixPath(p).split('/').map((val, i) => i <= 1 && /^[a-z]:$/i.test(val) ? val : encodeURIComponent(val)).join('/'); + if (/^[a-z]:[\\\/]/i.test(p)) { + p = '/' + p; + } + p = p.split(/[\\\/]/g).map((val, i) => i <= 1 && /^[a-z]:$/i.test(val) ? val : encodeURIComponent(val)).join('/'); return normalizeUri(ret + p); } @@ -124,6 +127,14 @@ export function uri2path(uri: string): string { return uri; } +export function uriToLocalPath(uri: string): string { + uri = uri.substring('file://'.length); + if (/^\/[a-z]:\//i.test(uri)) { + uri = uri.substring(1); + } + return uri.split('/').map(decodeURIComponent).join(path.sep); +} + export function isLocalUri(uri: string): boolean { return uri.startsWith('file://'); }