diff --git a/package-lock.json b/package-lock.json index 2c94b516651a..591e6f79bd5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19635,6 +19635,13 @@ "requires": { "vscode-languageserver-protocol": "3.14.1", "vscode-uri": "^1.0.6" + }, + "dependencies": { + "vscode-uri": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", + "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" + } } }, "vscode-languageserver-protocol": { @@ -19661,11 +19668,6 @@ "https-proxy-agent": "^2.2.1" } }, - "vscode-uri": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.8.tgz", - "integrity": "sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ==" - }, "vsls": { "version": "0.3.1291", "resolved": "https://registry.npmjs.org/vsls/-/vsls-0.3.1291.tgz", diff --git a/src/datascience-ui/interactive-common/mainState.ts b/src/datascience-ui/interactive-common/mainState.ts index 49ca98e6a586..687cffe8904b 100644 --- a/src/datascience-ui/interactive-common/mainState.ts +++ b/src/datascience-ui/interactive-common/mainState.ts @@ -211,10 +211,14 @@ export function generateCells(filePath: string, repetitions: number): ICell[] { for (let i = 0; i < repetitions; i += 1) { cellData = [...cellData, ...generateCellData()]; } + // Dynamically require vscode, this is testing code. + // Obfuscate the import to prevent webpack from picking this up. + // tslint:disable-next-line: no-eval (webpack is smart enough to look for `require` and `eval`). + const Uri = eval('req' + 'uire')('vscode').Uri; return cellData.map((data: nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | IMessageCell, key: number) => { return { id: key.toString(), - file: path.join(filePath, 'foo.py'), + file: Uri.file(path.join(filePath, 'foo.py')).fsPath, line: 1, state: key === cellData.length - 1 ? CellState.executing : CellState.finished, type: key === 3 ? 'preview' : 'execute', diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts index c6d4771f7e60..2930e5769de1 100644 --- a/src/test/activation/activationService.unit.test.ts +++ b/src/test/activation/activationService.unit.test.ts @@ -380,7 +380,7 @@ suite('Activation - ActivationService', () => { resource: Resource ) { activator - .setup(a => a.activate(resource)) + .setup(a => a.activate(TypeMoq.It.isValue(resource))) .returns(() => Promise.resolve()) .verifiable(TypeMoq.Times.once()); lsNotSupportedDiagnosticService diff --git a/src/test/datascience/datascience.unit.test.ts b/src/test/datascience/datascience.unit.test.ts index 38cbfaa36550..5cae8974efa4 100644 --- a/src/test/datascience/datascience.unit.test.ts +++ b/src/test/datascience/datascience.unit.test.ts @@ -42,13 +42,13 @@ suite('Data Science Tests', () => { assert.equal(expandWorkingDir(undefined, 'bar/foo.baz', inst), 'bar'); assert.equal(expandWorkingDir(undefined, 'bar/bip/foo.baz', inst), 'bar/bip'); - assert.equal(expandWorkingDir('${file}', 'bar/bip/foo.baz', inst), 'bar/bip/foo.baz'); - assert.equal(expandWorkingDir('${fileDirname}', 'bar/bip/foo.baz', inst), 'bar/bip'); + assert.equal(expandWorkingDir('${file}', 'bar/bip/foo.baz', inst), Uri.file('bar/bip/foo.baz').fsPath); + assert.equal(expandWorkingDir('${fileDirname}', 'bar/bip/foo.baz', inst), Uri.file('bar/bip').fsPath); assert.equal(expandWorkingDir('${relativeFile}', 'test/xyz/bip/foo.baz', inst), relativeFilePath); assert.equal(expandWorkingDir('${relativeFileDirname}', 'test/xyz/bip/foo.baz', inst), relativeFileDir); - assert.equal(expandWorkingDir('${cwd}', 'test/xyz/bip/foo.baz', inst), 'test/bar'); - assert.equal(expandWorkingDir('${workspaceFolder}', 'test/xyz/bip/foo.baz', inst), 'test/bar'); - assert.equal(expandWorkingDir('${cwd}-${file}', 'bar/bip/foo.baz', inst), 'test/bar-bar/bip/foo.baz'); + assert.equal(expandWorkingDir('${cwd}', 'test/xyz/bip/foo.baz', inst), Uri.file('test/bar').fsPath); + assert.equal(expandWorkingDir('${workspaceFolder}', 'test/xyz/bip/foo.baz', inst), Uri.file('test/bar').fsPath); + assert.equal(expandWorkingDir('${cwd}-${file}', 'bar/bip/foo.baz', inst), `${Uri.file('test/bar').fsPath}-${Uri.file('bar/bip/foo.baz').fsPath}`); }); test('input history', async () => { diff --git a/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts b/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts index dd8c43068637..8fde0bd86f40 100644 --- a/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts +++ b/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts @@ -3,7 +3,7 @@ 'use strict'; import { assert } from 'chai'; import * as TypeMoq from 'typemoq'; -import { Position, Range } from 'vscode'; +import { Position, Range, Uri } from 'vscode'; import { IDebugService } from '../../../client/common/application/types'; import { IConfigurationService, IDataScienceSettings, IPythonSettings } from '../../../client/common/types'; @@ -52,7 +52,7 @@ suite('CellHashProvider Unit Tests', () => { function sendCode(code: string, line: number, file?: string): Promise { const cell: ICell = { - file: file ? file : 'foo.py', + file: Uri.file(file ? file : 'foo.py').fsPath, line, data: { source: code, @@ -279,8 +279,8 @@ suite('CellHashProvider Unit Tests', () => { // Execution count should go up, but still only have two cells. const hashes = hashProvider.getHashes(); assert.equal(hashes.length, 2, 'Wrong number of hashes'); - const fooHash = hashes.find(h => h.file === 'foo.py'); - const barHash = hashes.find(h => h.file === 'bar.py'); + const fooHash = hashes.find(h => h.file === Uri.file('foo.py').fsPath); + const barHash = hashes.find(h => h.file === Uri.file('bar.py').fsPath); assert.ok(fooHash, 'No hash for foo.py'); assert.ok(barHash, 'No hash for bar.py'); assert.equal(fooHash!.hashes.length, 2, 'Not enough hashes found'); diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts index 420a09e6cfe6..35ef07ecc483 100644 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ b/src/test/datascience/editor-integration/codewatcher.unit.test.ts @@ -5,7 +5,7 @@ // Disable whitespace / multiline as we use that to pass in our fake file strings import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextEditor } from 'vscode'; +import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextEditor, Uri } from 'vscode'; import { ICommandManager, IDebugService, IDocumentManager } from '../../../client/common/application/types'; import { PythonSettings } from '../../../client/common/configSettings'; @@ -176,7 +176,7 @@ suite('DataScience Code Watcher Unit Tests', () => { } test('Add a file with just a #%% mark to a code watcher', () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%%`; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); @@ -197,7 +197,7 @@ suite('DataScience Code Watcher Unit Tests', () => { }); test('Add a file without a mark to a code watcher', () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `dummy`; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); @@ -217,7 +217,7 @@ suite('DataScience Code Watcher Unit Tests', () => { }); test('Add a file with multiple marks to a code watcher', () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `first line @@ -248,7 +248,7 @@ fourth line`; }); test('Add a file with custom marks to a code watcher', () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `first line @@ -286,7 +286,7 @@ fourth line }); test('Make sure invalid regex from a user still work', () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `first line @@ -324,7 +324,7 @@ fourth line }); test('Test the RunCell command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const testString = '#%%\ntesting'; const document = createDocument(testString, fileName, version, TypeMoq.Times.atLeastOnce(), true); @@ -350,7 +350,7 @@ fourth line }); test('Test the RunFileInteractive command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -364,7 +364,7 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code // RunFileInteractive should run the entire file in one block, not cell by cell like RunAllCells activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(inputText), - TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(0), TypeMoq.It.isAny(), TypeMoq.It.isAny() @@ -378,7 +378,7 @@ testing2`; // Command tests override getText, so just need the ranges here }); test('Test the RunAllCells command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -399,14 +399,14 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString1), - TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(0), TypeMoq.It.isAny(), TypeMoq.It.isAny() )).returns(() => Promise.resolve(true)).verifiable(TypeMoq.Times.once()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString2), - TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(2), TypeMoq.It.isAny(), TypeMoq.It.isAny() @@ -420,7 +420,7 @@ testing2`; // Command tests override getText, so just need the ranges here }); test('Test the RunCurrentCell command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -453,7 +453,7 @@ testing2`; }); test('Test the RunCellAndAllBelow command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -497,7 +497,7 @@ testing3`; }); test('Test the RunAllCellsAbove command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -541,7 +541,7 @@ testing2`; }); test('Test the RunToLine command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -574,7 +574,7 @@ testing1`; }); test('Test the RunToLine command with nothing on the lines', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = ` @@ -603,7 +603,7 @@ print('testing')`; }); test('Test the RunFromLine command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -639,7 +639,7 @@ testing3`; }); test('Test the RunSelection command', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -677,7 +677,7 @@ testing2`; }); test('Test the RunCellAndAdvance command with next cell', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -693,7 +693,7 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString), - TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(0), TypeMoq.It.is((ed: TextEditor) => { return textEditor.object === ed; @@ -732,7 +732,7 @@ testing2`; // Command tests override getText, so just need the ranges here test('CodeLens returned after settings changed is different', () => { // Create our document - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = '#%% foobar'; const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); @@ -764,7 +764,7 @@ testing2`; // Command tests override getText, so just need the ranges here }); test('Test the RunAllCellsAbove command with an error', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -808,7 +808,7 @@ testing2`; }); test('Test the RunAllCells command with an error', async () => { - const fileName = 'test.py'; + const fileName = Uri.file('test.py').fsPath; const version = 1; const inputText = `#%% @@ -829,14 +829,14 @@ testing2`; // Command tests override getText, so just need the ranges here // Set up our expected calls to add code activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString1), - TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(0), TypeMoq.It.isAny(), TypeMoq.It.isAny() )).returns(() => Promise.resolve(false)).verifiable(TypeMoq.Times.once()); activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString2), - TypeMoq.It.isValue('test.py'), + TypeMoq.It.isValue(fileName), TypeMoq.It.isValue(2), TypeMoq.It.isAny(), TypeMoq.It.isAny() diff --git a/src/test/datascience/editor-integration/helpers.ts b/src/test/datascience/editor-integration/helpers.ts index 384e185514a3..bd363fb16ba2 100644 --- a/src/test/datascience/editor-integration/helpers.ts +++ b/src/test/datascience/editor-integration/helpers.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. 'use strict'; import * as TypeMoq from 'typemoq'; -import { Range, TextDocument, TextLine } from 'vscode'; +import { Range, TextDocument, TextLine, Uri } from 'vscode'; // tslint:disable:max-func-body-length no-trailing-whitespace no-multiline-string // Disable whitespace / multiline as we use that to pass in our fake file strings @@ -18,7 +18,7 @@ export function createDocument(inputText: string, fileName: string, fileVersion: document.setup(d => d.languageId).returns(() => 'python'); // First set the metadata - document.setup(d => d.fileName).returns(() => fileName).verifiable(times); + document.setup(d => d.fileName).returns(() => Uri.file(fileName).fsPath).verifiable(times); document.setup(d => d.version).returns(() => fileVersion).verifiable(times); // Next add the lines in diff --git a/src/test/datascience/intellisense.unit.test.ts b/src/test/datascience/intellisense.unit.test.ts index 51d5418d7db7..fe1e860a53fb 100644 --- a/src/test/datascience/intellisense.unit.test.ts +++ b/src/test/datascience/intellisense.unit.test.ts @@ -5,6 +5,7 @@ import { expect } from 'chai'; import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; import * as TypeMoq from 'typemoq'; +import { Uri } from 'vscode'; import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../client/activation/types'; import { IWorkspaceService } from '../../client/common/application/types'; import { PythonSettings } from '../../client/common/configSettings'; @@ -96,7 +97,7 @@ suite('DataScience Intellisense Unit Tests', () => { } function addCell(code: string, id: string): Promise { - return sendMessage(InteractiveWindowMessages.AddCell, { fullText: code, currentText: code, file: 'foo.py', id }); + return sendMessage(InteractiveWindowMessages.AddCell, { fullText: code, currentText: code, file: Uri.file('foo.py').fsPath, id }); } function updateCell(newCode: string, oldCode: string, id: string): Promise { diff --git a/src/test/datascience/interactiveWindow.functional.test.tsx b/src/test/datascience/interactiveWindow.functional.test.tsx index 90b70372488a..6a71f86444a6 100644 --- a/src/test/datascience/interactiveWindow.functional.test.tsx +++ b/src/test/datascience/interactiveWindow.functional.test.tsx @@ -7,7 +7,7 @@ import { parse } from 'node-html-parser'; import * as os from 'os'; import * as path from 'path'; import * as TypeMoq from 'typemoq'; -import { Disposable, Selection, TextDocument, TextEditor } from 'vscode'; +import { Disposable, Selection, TextDocument, TextEditor, Uri } from 'vscode'; import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; import { createDeferred, waitForPromise } from '../../client/common/utils/async'; @@ -254,7 +254,7 @@ for _ in range(50): const docManager = TypeMoq.Mock.ofType(); const visibleEditor = TypeMoq.Mock.ofType(); const dummyDocument = TypeMoq.Mock.ofType(); - dummyDocument.setup(d => d.fileName).returns(() => 'foo.py'); + dummyDocument.setup(d => d.fileName).returns(() => Uri.file('foo.py').fsPath); visibleEditor.setup(v => v.show()).returns(() => showedEditor.resolve()); visibleEditor.setup(v => v.revealRange(TypeMoq.It.isAny())).returns(noop); visibleEditor.setup(v => v.document).returns(() => dummyDocument.object); @@ -394,7 +394,7 @@ for _ in range(50): const updatePromise = waitForUpdate(wrapper, InteractivePanel); // Send some code to the interactive window - await interactiveWindow.addCode('a=1\na', 'foo.py', 2); + await interactiveWindow.addCode('a=1\na', Uri.file('foo.py').fsPath, 2); // Wait for the render to go through await updatePromise; @@ -533,13 +533,13 @@ for _ in range(50): const executed = createDeferred(); // We have to wait until the execute goes through before we reset. interactiveWindow.onExecutedCode(() => executed.resolve()); - const added = interactiveWindow.addCode('import time\r\ntime.sleep(1000)', 'foo', 0); + const added = interactiveWindow.addCode('import time\r\ntime.sleep(1000)', Uri.file('foo').fsPath, 0); await executed.promise; await interactiveWindow.restartKernel(); await added; // Now see if our wrapper still works. Interactive window should have forced a restart - await interactiveWindow.addCode('a=1\na', 'foo', 0); + await interactiveWindow.addCode('a=1\na', Uri.file('foo').fsPath, 0); verifyHtmlOnCell(wrapper, 'InteractiveCell', '1', CellPosition.Last); }, () => { return ioc; }); diff --git a/src/test/datascience/interactiveWindowTestHelpers.tsx b/src/test/datascience/interactiveWindowTestHelpers.tsx index 05bf07e76361..a5d02690d572 100644 --- a/src/test/datascience/interactiveWindowTestHelpers.tsx +++ b/src/test/datascience/interactiveWindowTestHelpers.tsx @@ -5,6 +5,7 @@ import * as assert from 'assert'; import { ReactWrapper } from 'enzyme'; import * as React from 'react'; +import { Uri } from 'vscode'; import { IInteractiveWindow, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; import { InteractivePanel } from '../../datascience-ui/history-react/interactivePanel'; import { DataScienceIocContainer } from './dataScienceIocContainer'; @@ -46,7 +47,7 @@ export async function addCode(ioc: DataScienceIocContainer, wrapper: ReactWrappe // 5) Status finished return getInteractiveCellResults(wrapper, expectedRenderCount, async () => { const history = await getOrCreateInteractiveWindow(ioc); - const success = await history.addCode(code, 'foo.py', 2); + const success = await history.addCode(code, Uri.file('foo.py').fsPath, 2); if (expectError) { assert.equal(success, false, `${code} did not produce an error`); } diff --git a/src/test/datascience/liveshare.functional.test.tsx b/src/test/datascience/liveshare.functional.test.tsx index 1ff19cafed39..32380c0d9ed9 100644 --- a/src/test/datascience/liveshare.functional.test.tsx +++ b/src/test/datascience/liveshare.functional.test.tsx @@ -137,7 +137,7 @@ suite('DataScience LiveShare tests', () => { return waitForResults(role, async (both: boolean) => { if (!both) { const history = await getOrCreateInteractiveWindow(role); - await history.addCode(code, 'foo.py', 2); + await history.addCode(code, Uri.file('foo.py').fsPath, 2); } else { // Add code to the apropriate container const host = await getOrCreateInteractiveWindow(vsls.Role.Host); @@ -145,9 +145,9 @@ suite('DataScience LiveShare tests', () => { // Make sure guest is still creatable if (isSessionStarted(vsls.Role.Guest)) { const guest = await getOrCreateInteractiveWindow(vsls.Role.Guest); - (role === vsls.Role.Host ? await host.addCode(code, 'foo.py', 2) : await guest.addCode(code, 'foo.py', 2)); + (role === vsls.Role.Host ? await host.addCode(code, Uri.file('foo.py').fsPath, 2) : await guest.addCode(code, Uri.file('foo.py').fsPath, 2)); } else { - await host.addCode(code, 'foo.py', 2); + await host.addCode(code, Uri.file('foo.py').fsPath, 2); } } }, expectedRenderCount); @@ -319,7 +319,7 @@ suite('DataScience LiveShare tests', () => { await startSession(vsls.Role.Guest); // Create a document on the guest - guestContainer!.addDocument('#%%\na=1\na', 'foo.py'); + guestContainer!.addDocument('#%%\na=1\na', Uri.file('foo.py').fsPath); guestContainer!.get(IDocumentManager).showTextDocument(Uri.file('foo.py')); // Attempt to export a file from the guest by running an ExportFileAndOutputAsNotebook @@ -327,7 +327,7 @@ suite('DataScience LiveShare tests', () => { assert.ok(executePromise, 'Export file did not return a promise'); const savedUri = await executePromise; assert.ok(savedUri, 'Uri not returned from export'); - assert.equal(savedUri.fsPath, 'test.ipynb', 'Export did not work'); + assert.equal(savedUri.fsPath, Uri.file('test.ipynb').fsPath, 'Export did not work'); assert.ok(outputContents, 'Output not exported'); assert.ok(outputContents!.includes('data'), 'Output is empty'); }); diff --git a/src/test/datascience/mockDocumentManager.ts b/src/test/datascience/mockDocumentManager.ts index 3c8052fc2c08..a3ded873a19b 100644 --- a/src/test/datascience/mockDocumentManager.ts +++ b/src/test/datascience/mockDocumentManager.ts @@ -94,7 +94,7 @@ export class MockDocumentManager implements IDocumentManager { } public changeDocument(file: string, changes: { range: Range; newText: string }[]) { - const doc = this.textDocuments.find(d => d.fileName === file) as MockDocument; + const doc = this.textDocuments.find(d => d.uri.fsPath === Uri.file(file).fsPath) as MockDocument; if (doc) { const contentChanges = changes.map(c => { const startOffset = doc.offsetAt(c.range.start); diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts index d2671fd8d382..e5320ed826c0 100644 --- a/src/test/datascience/mockJupyterManager.ts +++ b/src/test/datascience/mockJupyterManager.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import { Observable } from 'rxjs/Observable'; import * as TypeMoq from 'typemoq'; import * as uuid from 'uuid/v4'; -import { EventEmitter } from 'vscode'; +import { EventEmitter, Uri } from 'vscode'; import { CancellationToken } from 'vscode-jsonrpc'; import { Cancellation } from '../../client/common/cancellation'; @@ -107,13 +107,26 @@ export class MockJupyterManager implements IJupyterSessionManager { this.addCell('matplotlib.style.use(\'dark_background\')'); this.addCell(`matplotlib.rcParams.update(${Identifiers.MatplotLibDefaultParams})`); this.addCell(`%cd "${path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')}"`); + // When we have windows file names, we replace `\` with `\\`. + // Code is as follows `await this.notebook.execute(`__file__ = '${file.replace(/\\/g, '\\\\')}'`, file, line, uuid(), undefined, true); + // Found in src\client\datascience\interactive-common\interactiveBase.ts. + this.addCell(`%cd "${Uri.file(path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')).fsPath}`); this.addCell('import sys\r\nsys.version', '1.1.1.1'); this.addCell('import sys\r\nsys.executable', 'python'); this.addCell('import notebook\r\nnotebook.version_info', '1.1.1.1'); - this.addCell(`__file__ = 'foo.py'`); - this.addCell(`__file__ = 'bar.py'`); - this.addCell(`__file__ = 'foo'`); - this.addCell(`__file__ = 'test.py'`); + + this.addCell(`__file__ = '${Uri.file('foo.py').fsPath}'`); + this.addCell(`__file__ = '${Uri.file('bar.py').fsPath}'`); + this.addCell(`__file__ = '${Uri.file('foo').fsPath}'`); + this.addCell(`__file__ = '${Uri.file('test.py').fsPath}'`); + + // When we have windows file names, we replace `\` with `\\`. + // Code is as follows `await this.notebook.execute(`__file__ = '${file.replace(/\\/g, '\\\\')}'`, file, line, uuid(), undefined, true); + // Found in src\client\datascience\interactive-common\interactiveBase.ts. + this.addCell(`__file__ = '${Uri.file('foo.py').fsPath.replace(/\\/g, '\\\\')}'`); + this.addCell(`__file__ = '${Uri.file('bar.py').fsPath.replace(/\\/g, '\\\\')}'`); + this.addCell(`__file__ = '${Uri.file('foo').fsPath.replace(/\\/g, '\\\\')}'`); + this.addCell(`__file__ = '${Uri.file('test.py').fsPath.replace(/\\/g, '\\\\')}'`); } public getConnInfo(): IConnection { @@ -171,7 +184,7 @@ export class MockJupyterManager implements IJupyterSessionManager { } public addContinuousOutputCell(code: string, resultGenerator: (cancelToken: CancellationToken) => Promise<{ result: string; haveMore: boolean }>) { - const cells = generateCells(undefined, code, 'foo.py', 1, true, uuid()); + const cells = generateCells(undefined, code, Uri.file('foo.py').fsPath, 1, true, uuid()); cells.forEach(c => { const key = concatMultilineString(c.data.source).replace(LineFeedRegEx, ''); if (c.data.cell_type === 'code') { @@ -202,7 +215,7 @@ export class MockJupyterManager implements IJupyterSessionManager { } public addCell(code: string, result?: undefined | string | number | nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError, mimeType?: string) { - const cells = generateCells(undefined, code, 'foo.py', 1, true, uuid()); + const cells = generateCells(undefined, code, Uri.file('foo.py').fsPath, 1, true, uuid()); cells.forEach(c => { const cellMatcher = new CellMatcher(); const key = cellMatcher.stripFirstMarker(concatMultilineString(c.data.source)).replace(LineFeedRegEx, ''); diff --git a/src/test/datascience/mockLiveShare.ts b/src/test/datascience/mockLiveShare.ts index 031826718886..435c6e1a62e1 100644 --- a/src/test/datascience/mockLiveShare.ts +++ b/src/test/datascience/mockLiveShare.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. 'use strict'; import { inject, injectable } from 'inversify'; -import * as path from 'path'; import * as uuid from 'uuid/v4'; import { CancellationToken, CancellationTokenSource, Disposable, Event, EventEmitter, TreeDataProvider, Uri } from 'vscode'; import * as vsls from 'vsls/vscode'; @@ -325,8 +324,7 @@ class MockLiveShare implements vsls.LiveShare, vsls.Session, vsls.Peer, IDisposa throw new Error(`Not a workspace file URI: ${localUri}`); } - const file = localUri.fsPath.includes('/') ? path.basename(localUri.fsPath) : localUri.fsPath; - return Uri.parse(`vsls:${file}`); + return Uri.parse(`vsls:${localUri.fsPath}`); } public convertSharedUriToLocal(sharedUri: Uri): Uri { checkArg(sharedUri, 'sharedUri', 'uri'); diff --git a/src/test/datascience/nativeEditor.functional.test.tsx b/src/test/datascience/nativeEditor.functional.test.tsx index 471cf7eb6b82..56b477161f92 100644 --- a/src/test/datascience/nativeEditor.functional.test.tsx +++ b/src/test/datascience/nativeEditor.functional.test.tsx @@ -170,7 +170,7 @@ suite('DataScience Native Editor', () => { const docManager = TypeMoq.Mock.ofType(); const visibleEditor = TypeMoq.Mock.ofType(); const dummyDocument = TypeMoq.Mock.ofType(); - dummyDocument.setup(d => d.fileName).returns(() => 'foo.py'); + dummyDocument.setup(d => d.fileName).returns(() => Uri.file('foo.py').fsPath); visibleEditor.setup(v => v.show()).returns(() => showedEditor.resolve()); visibleEditor.setup(v => v.revealRange(TypeMoq.It.isAny())).returns(noop); visibleEditor.setup(v => v.document).returns(() => dummyDocument.object); diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts index c88c7a996661..e3b97e1b2951 100644 --- a/src/test/datascience/notebook.functional.test.ts +++ b/src/test/datascience/notebook.functional.test.ts @@ -636,7 +636,7 @@ suite('DataScience notebook tests', () => { let finishedBefore = false; const finishedPromise = createDeferred(); let error; - const observable = notebook!.executeObservable(code, 'foo.py', 0, uuid(), false); + const observable = notebook!.executeObservable(code, Uri.file('foo.py').fsPath, 0, uuid(), false); observable.subscribe(c => { if (c.length > 0 && c[0].state === CellState.error) { finishedBefore = !interrupted; diff --git a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts index 2d1d174ccd9a..0127e3fc70f1 100644 --- a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts @@ -16,6 +16,7 @@ import { OSType } from '../../../../../client/common/utils/platform'; import { AttachConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/attach'; import { AttachRequestArguments, DebugOptions } from '../../../../../client/debugger/types'; import { IServiceContainer } from '../../../../../client/ioc/types'; +import { getOSType } from '../../../../common'; import { getInfoPerOS, setUpOSMocks } from './common'; getInfoPerOS().forEach(([osName, osType, path]) => { @@ -174,7 +175,10 @@ getInfoPerOS().forEach(([osName, osType, path]) => { expect(pathMappings![0].localRoot).to.be.equal(workspaceFolder.uri.fsPath); expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); }); - test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}'`, async () => { + test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}'`, async function () { + if (getOSType() !== OSType.Windows || osType !== OSType.Windows){ + return this.skip(); + } const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -185,13 +189,32 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - const expected = osType === OSType.Windows - ? path.join('c:', 'Debug', 'Python_Path') - : path.join('C:', 'Debug', 'Python_Path'); + const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path')).fsPath; expect(pathMappings![0].localRoot).to.be.equal(expected); expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); }); - test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}' and with existing path mappings`, async () => { + test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}'`, async function () { + if (getOSType() === OSType.Windows || osType === OSType.Windows){ + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, host, request: 'attach' } as any as DebugConfiguration); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + + const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path')).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal(workspaceFolder.uri.fsPath); + }); + test(`Ensure drive letter is lower cased for local path mappings on Windows when host is '${host}' and with existing path mappings`, async function () { + if (getOSType() !== OSType.Windows || osType !== OSType.Windows){ + return this.skip(); + } const activeFile = 'xyz.py'; const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); setupActiveEditor(activeFile, PYTHON_LANGUAGE); @@ -203,9 +226,26 @@ getInfoPerOS().forEach(([osName, osType, path]) => { const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, pathMappings: debugPathMappings, host, request: 'attach' } as any as DebugConfiguration); const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - const expected = osType === OSType.Windows - ? path.join('c:', 'Debug', 'Python_Path', localRoot) - : path.join('C:', 'Debug', 'Python_Path', localRoot); + const expected = Uri.file(path.join('c:', 'Debug', 'Python_Path', localRoot)).fsPath; + expect(pathMappings![0].localRoot).to.be.equal(expected); + expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); + }); + test(`Ensure drive letter is not lower cased for local path mappings on non-Windows when host is '${host}' and with existing path mappings`, async function () { + if (getOSType() === OSType.Windows || osType === OSType.Windows){ + return this.skip(); + } + const activeFile = 'xyz.py'; + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor(activeFile, PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + + const localRoot = `Debug_PythonPath_${new Date().toString()}`; + const debugPathMappings = [ { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }]; + const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, pathMappings: debugPathMappings, host, request: 'attach' } as any as DebugConfiguration); + const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + + const expected = Uri.file(path.join('USR', 'Debug', 'Python_Path', localRoot)).fsPath; expect(pathMappings![0].localRoot).to.be.equal(expected); expect(pathMappings![0].remoteRoot).to.be.equal('/app/'); }); diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index 016b53d06447..eb4694642517 100644 --- a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts +++ b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts @@ -20,6 +20,7 @@ import { IDebugEnvironmentVariablesService } from '../../../../../client/debugge import { LaunchConfigurationResolver } from '../../../../../client/debugger/extension/configuration/resolvers/launch'; import { DebugOptions, LaunchRequestArguments } from '../../../../../client/debugger/types'; import { IInterpreterHelper } from '../../../../../client/interpreter/contracts'; +import { getOSType } from '../../../../common'; import { getInfoPerOS, setUpOSMocks } from './common'; getInfoPerOS().forEach(([osName, osType, path]) => { @@ -379,33 +380,63 @@ getInfoPerOS().forEach(([osName, osType, path]) => { remoteRoot: '.' }]); }); - test('Ensure drive letter is lower cased for local path mappings on Windows when with existing path mappings', async () => { + test('Ensure drive letter is lower cased for local path mappings on Windows when with existing path mappings', async function () { + if (getOSType() !== OSType.Windows || osType !== OSType.Windows){ + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } const workspaceFolder = createMoqWorkspaceFolder(path.join('C:', 'Debug', 'Python_Path')); setupActiveEditor('spam.py', PYTHON_LANGUAGE); const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const localRoot = path.join(workspaceFolder.uri.fsPath, 'app'); + const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; const debugConfig = await debugProvider.resolveDebugConfiguration!( workspaceFolder, { request: 'launch', pathMappings: [{ - localRoot: localRoot, + localRoot, remoteRoot: '/app/' }] } as any as DebugConfiguration ); const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; - const expected = osType === OSType.Windows - ? `c${localRoot.substring(1)}` - : localRoot; + const expected = Uri.file(`c${localRoot.substring(1)}`).fsPath; expect(pathMappings).to.deep.equal([{ localRoot: expected, remoteRoot: '/app/' }]); }); + test('Ensure drive letter is not lower cased for local path mappings on non-Windows when with existing path mappings', async function () { + if (getOSType() === OSType.Windows || osType === OSType.Windows){ + // tslint:disable-next-line: no-invalid-this + return this.skip(); + } + const workspaceFolder = createMoqWorkspaceFolder(path.join('USR', 'Debug', 'Python_Path')); + setupActiveEditor('spam.py', PYTHON_LANGUAGE); + const defaultWorkspace = path.join('usr', 'desktop'); + setupWorkspaces([defaultWorkspace]); + const localRoot = Uri.file(path.join(workspaceFolder.uri.fsPath, 'app')).fsPath; + + const debugConfig = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + { + request: 'launch', + pathMappings: [{ + localRoot, + remoteRoot: '/app/' + }] + } as any as DebugConfiguration + ); + + const pathMappings = (debugConfig as LaunchRequestArguments).pathMappings; + expect(pathMappings).to.deep.equal([{ + localRoot, + remoteRoot: '/app/' + }]); + }); test('Ensure local path mappings are not modified when not pointing to a local drive', async () => { const workspaceFolder = createMoqWorkspaceFolder(path.join('Server', 'Debug', 'Python_Path')); setupActiveEditor('spam.py', PYTHON_LANGUAGE); diff --git a/src/test/mocks/vsc/charCode.ts b/src/test/mocks/vsc/charCode.ts new file mode 100644 index 000000000000..98bbd400de7f --- /dev/null +++ b/src/test/mocks/vsc/charCode.ts @@ -0,0 +1,427 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* tslint:disable */ + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030A, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030B, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030C, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030D, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030E, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030F, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031A, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031B, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031C, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031D, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031E, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031F, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032A, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032B, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032C, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032D, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032E, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032F, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033A, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033B, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033C, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033D, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033E, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033F, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034A, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034B, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034C, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034D, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034E, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034F, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035A, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035B, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035C, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035D, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035E, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035F, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036A, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036B, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036C, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036D, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036E, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036F, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR_2028 = 8232, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00A8, // U+00A8 DIAERESIS + U_MACRON = 0x00AF, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00B4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00B8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02D4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02D5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02D8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02D9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02DA, // U+02DA RING ABOVE + U_OGONEK = 0x02DB, // U+02DB OGONEK + U_SMALL_TILDE = 0x02DC, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02DD, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ED, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02F3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02F7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02F8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02FD, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1FBD, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1FBF, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1FC0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1FCD, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1FCE, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1FDD, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1FDE, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1FEF, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA + + + U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279 +} diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index 117fa5e8b4d6..c7df9e7c2148 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -11,53 +11,11 @@ import * as vscode from 'vscode'; // export * from './position'; // export * from './selection'; export * from './extHostedTypes'; +export * from './uri'; export namespace vscMock { - // This is one of the very few classes that we need in our unit tests. - // It is constructed in a number of places, and this is required for verification. - // Using mocked objects for verfications does not work in typemoq. - export class Uri implements vscode.Uri { - private static _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; - private static _empty = ''; - - private constructor( - public readonly scheme: string, - public readonly authority: string, - public readonly path: string, - public readonly query: string, - public readonly fragment: string, - public readonly fsPath: string - ) {} - public static file(path: string): Uri { - return new Uri('file', '', path, '', '', path); - } - public static parse(value: string): Uri { - const match = this._regexp.exec(value); - if (!match) { - return new Uri('', '', '', '', '', ''); - } - return new Uri( - match[2] || this._empty, - decodeURIComponent(match[4] || this._empty), - decodeURIComponent(match[5] || this._empty), - decodeURIComponent(match[7] || this._empty), - decodeURIComponent(match[9] || this._empty), - decodeURIComponent(match[5] || this._empty) - ); - } - public with(_change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): vscode.Uri { - throw new Error('Not implemented'); - } - public toString(_skipEncoding?: boolean): string { - return this.fsPath; - } - public toJSON(): any { - return this.fsPath; - } - } - export class Disposable { - constructor(private callOnDispose: Function) {} + constructor(private callOnDispose: Function) { } public dispose(): any { if (this.callOnDispose) { this.callOnDispose(); @@ -195,7 +153,7 @@ export namespace vscMock { public static readonly SourceOrganizeImports: CodeActionKind = new CodeActionKind('source.organize.imports'); public static readonly SourceFixAll: CodeActionKind = new CodeActionKind('source.fix.all'); - private constructor(private _value: string) {} + private constructor(private _value: string) { } public append(parts: string): CodeActionKind { return new CodeActionKind(`${this._value}.${parts}`); @@ -220,9 +178,9 @@ export namespace vscMock { } export class DebugAdapterServer { - constructor(public readonly port: number, public readonly host?: string) {} + constructor(public readonly port: number, public readonly host?: string) { } } export class DebugAdapterExecutable { - constructor(public readonly command: string, public readonly args: string[] = [], public readonly options?: DebugAdapterExecutableOptions) {} + constructor(public readonly command: string, public readonly args: string[] = [], public readonly options?: DebugAdapterExecutableOptions) { } } } diff --git a/src/test/mocks/vsc/uri.ts b/src/test/mocks/vsc/uri.ts index 0e7613d2bae5..9ccadab42f39 100644 --- a/src/test/mocks/vsc/uri.ts +++ b/src/test/mocks/vsc/uri.ts @@ -4,32 +4,43 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -export namespace vscUri { - const platform = { - isWindows: /^win/.test(process.platform) - }; +/* tslint:disable */ - // tslint:disable:all +import { CharCode } from './charCode'; - function _encode(ch: string): string { - return '%' + ch.charCodeAt(0).toString(16).toUpperCase(); - } +export namespace vscUri { + const isWindows = /^win/.test(process.platform); + /*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ - // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent - function encodeURIComponent2(str: string): string { - return encodeURIComponent(str).replace(/[!'()*]/g, _encode); - } + const _schemePattern = /^\w[\w\d+.-]*$/; + const _singleSlashStart = /^\//; + const _doubleSlashStart = /^\/\//; - function encodeNoop(str: string): string { - return str.replace(/[#?]/, _encode); + let _throwOnMissingSchema: boolean = true; + + /** + * @internal + */ + export function setUriThrowOnMissingScheme(value: boolean): boolean { + const old = _throwOnMissingSchema; + _throwOnMissingSchema = value; + return old; } + function _validateUri(ret: URI, _strict?: boolean): void { - const _schemePattern = /^\w[\w\d+.-]*$/; - const _singleSlashStart = /^\//; - const _doubleSlashStart = /^\/\//; + // scheme, must be set + // if (!ret.scheme) { + // // if (_strict || _throwOnMissingSchema) { + // // throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`); + // // } else { + // console.warn(`[UriError]: Scheme is missing: {scheme: "", authority: "${ret.authority}", path: "${ret.path}", query: "${ret.query}", fragment: "${ret.fragment}"}`); + // // } + // } - function _validateUri(ret: URI): void { // scheme, https://tools.ietf.org/html/rfc3986#section-3.1 // ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) if (ret.scheme && !_schemePattern.test(ret.scheme)) { @@ -54,16 +65,50 @@ export namespace vscUri { } } + // for a while we allowed uris *without* schemes and this is the migration + // for them, e.g. an uri without scheme and without strict-mode warns and falls + // back to the file-scheme. that should cause the least carnage and still be a + // clear warning + function _schemeFix(scheme: string, _strict: boolean): string { + if (_strict || _throwOnMissingSchema) { + return scheme || _empty; + } + if (!scheme) { + // tslint:disable-next-line: no-console + console.trace('BAD uri lacks scheme, falling back to file-scheme.'); + scheme = 'file'; + } + return scheme; + } + + // implements a bit of https://tools.ietf.org/html/rfc3986#section-5 + function _referenceResolution(scheme: string, path: string): string { + + // the slash-character is our 'default base' as we don't + // support constructing URIs relative to other URIs. This + // also means that we alter and potentially break paths. + // see https://tools.ietf.org/html/rfc3986#section-5.1.4 + switch (scheme) { + case 'https': + case 'http': + case 'file': + if (!path) { + path = _slash; + } else if (path[0] !== _slash) { + path = _slash + path; + } + break; + } + return path; + } + const _empty = ''; const _slash = '/'; const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; - const _driveLetterPath = /^\/[a-zA-Z]:/; - const _upperCaseDrive = /^(\/)?([A-Z]:)/; - const _driveLetter = /^[a-zA-Z]:/; /** * Uniform Resource Identifier (URI) http://tools.ietf.org/html/rfc3986. - * This class is a simple parser which creates the basic component paths + * This class is a simple parser which creates the basic component parts * (http://tools.ietf.org/html/rfc3986#section-3) with minimal validation * and encoding. * @@ -74,9 +119,8 @@ export namespace vscUri { * | _____________________|__ * / \ / \ * urn:example:animal:ferret:nose - * - * */ + // tslint:disable-next-line: no-use-before-declare export class URI implements UriComponents { static isUri(thing: any): thing is URI { @@ -90,7 +134,10 @@ export namespace vscUri { && typeof (thing).fragment === 'string' && typeof (thing).path === 'string' && typeof (thing).query === 'string' - && typeof (thing).scheme === 'string'; + && typeof (thing).scheme === 'string' + && typeof (thing).fsPath === 'function' + && typeof (thing).with === 'function' + && typeof (thing).toString === 'function'; } /** @@ -123,7 +170,7 @@ export namespace vscUri { /** * @internal */ - protected constructor(scheme: string, authority: string, path: string, query: string, fragment: string); + protected constructor(scheme: string, authority?: string, path?: string, query?: string, fragment?: string, _strict?: boolean); /** * @internal @@ -133,7 +180,7 @@ export namespace vscUri { /** * @internal */ - protected constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string) { + protected constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string, _strict: boolean = false) { if (typeof schemeOrData === 'object') { this.scheme = schemeOrData.scheme || _empty; @@ -145,12 +192,13 @@ export namespace vscUri { // that creates uri components. // _validateUri(this); } else { - this.scheme = schemeOrData || _empty; + this.scheme = _schemeFix(schemeOrData, _strict); this.authority = authority || _empty; - this.path = path || _empty; + this.path = _referenceResolution(this.scheme, path || _empty); this.query = query || _empty; this.fragment = fragment || _empty; - _validateUri(this); + + _validateUri(this, _strict); } } @@ -158,44 +206,65 @@ export namespace vscUri { /** * Returns a string representing the corresponding file system path of this URI. - * Will handle UNC paths and normalize windows drive letters to lower-case. Also - * uses the platform specific path separator. Will *not* validate the path for - * invalid characters and semantics. Will *not* look at the scheme of this URI. - */ + * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the + * platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this URI. + * * The result shall *not* be used for display purposes but for accessing a file on disk. + * + * + * The *difference* to `URI#path` is the use of the platform specific separator and the handling + * of UNC paths. See the below sample of a file-uri with an authority (UNC path). + * + * ```ts + const u = URI.parse('file://server/c$/folder/file.txt') + u.authority === 'server' + u.path === '/shares/c$/file.txt' + u.fsPath === '\\server\c$\folder\file.txt' + ``` + * + * Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path, + * namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working + * with URIs that represent files on disk (`file` scheme). + */ get fsPath(): string { + // if (this.scheme !== 'file') { + // console.warn(`[UriError] calling fsPath with scheme ${this.scheme}`); + // } return _makeFsPath(this); } // ---- modify to new ------------------------- - public with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { + with(change: { scheme?: string; authority?: string | null; path?: string | null; query?: string | null; fragment?: string | null }): URI { if (!change) { return this; } let { scheme, authority, path, query, fragment } = change; - if (scheme === void 0) { + if (scheme === undefined) { scheme = this.scheme; } else if (scheme === null) { scheme = _empty; } - if (authority === void 0) { + if (authority === undefined) { authority = this.authority; } else if (authority === null) { authority = _empty; } - if (path === void 0) { + if (path === undefined) { path = this.path; } else if (path === null) { path = _empty; } - if (query === void 0) { + if (query === undefined) { query = this.query; } else if (query === null) { query = _empty; } - if (fragment === void 0) { + if (fragment === undefined) { fragment = this.fragment; } else if (fragment === null) { fragment = _empty; @@ -215,7 +284,13 @@ export namespace vscUri { // ---- parse & validate ------------------------ - public static parse(value: string): URI { + /** + * Creates a new URI from a string, e.g. `http://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @param value A string which represents an URI (see `URI#toString`). + */ + static parse(value: string, _strict: boolean = false): URI { const match = _regexp.exec(value); if (!match) { return new _URI(_empty, _empty, _empty, _empty, _empty); @@ -226,24 +301,46 @@ export namespace vscUri { decodeURIComponent(match[5] || _empty), decodeURIComponent(match[7] || _empty), decodeURIComponent(match[9] || _empty), + _strict ); } - public static file(path: string): URI { + /** + * Creates a new URI from a file system path, e.g. `c:\my\files`, + * `/usr/home`, or `\\server\share\some\path`. + * + * The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument + * as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as** + * `URI.parse('file://' + path)` because the path might contain characters that are + * interpreted (# and ?). See the following sample: + * ```ts + const good = URI.file('/coding/c#/project1'); + good.scheme === 'file'; + good.path === '/coding/c#/project1'; + good.fragment === ''; + const bad = URI.parse('file://' + '/coding/c#/project1'); + bad.scheme === 'file'; + bad.path === '/coding/c'; // path is now broken + bad.fragment === '/project1'; + ``` + * + * @param path A file system path (see `URI#fsPath`) + */ + static file(path: string): URI { let authority = _empty; // normalize to fwd-slashes on windows, // on other systems bwd-slashes are valid // filename character, eg /f\oo/ba\r.txt - if (platform.isWindows) { + if (isWindows) { path = path.replace(/\\/g, _slash); } // check for authority as used in UNC shares // or use the path as given if (path[0] === _slash && path[1] === _slash) { - let idx = path.indexOf(_slash, 2); + const idx = path.indexOf(_slash, 2); if (idx === -1) { authority = path.substring(2); path = _slash; @@ -253,24 +350,11 @@ export namespace vscUri { } } - // Ensure that path starts with a slash - // or that it is at least a slash - if (_driveLetter.test(path)) { - path = _slash + path; - - } else if (path[0] !== _slash) { - // tricky -> makes invalid paths - // but otherwise we have to stop - // allowing relative paths... - path = _slash + path; - } - return new _URI('file', authority, path, _empty, _empty); } - public static from(components: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { + static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI { return new _URI( - // @ts-ignore components.scheme, components.authority, components.path, @@ -282,52 +366,37 @@ export namespace vscUri { // ---- printing/externalize --------------------------- /** + * Creates a string representation for this URI. It's guaranteed that calling + * `URI.parse` with the result of this function creates an URI which is equal + * to this URI. + * + * * The result shall *not* be used for display purposes but for externalization or transport. + * * The result will be encoded using the percentage encoding and encoding happens mostly + * ignore the scheme-specific encoding rules. * * @param skipEncoding Do not encode the result, default is `false` */ - public toString(skipEncoding: boolean = false): string { + toString(skipEncoding: boolean = false): string { return _asFormatted(this, skipEncoding); } - public toJSON(): object { - const res = { - $mid: 1, - fsPath: this.fsPath, - external: this.toString(), - }; - - if (this.path) { - res.path = this.path; - } - - if (this.scheme) { - res.scheme = this.scheme; - } - - if (this.authority) { - res.authority = this.authority; - } - - if (this.query) { - res.query = this.query; - } - - if (this.fragment) { - res.fragment = this.fragment; - } - - return res; + toJSON(): UriComponents { + return this; } - static revive(data: UriComponents | any): URI { + static revive(data: UriComponents | URI): URI; + static revive(data: UriComponents | URI | undefined): URI | undefined; + static revive(data: UriComponents | URI | null): URI | null; + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null; + static revive(data: UriComponents | URI | undefined | null): URI | undefined | null { if (!data) { return data; } else if (data instanceof URI) { return data; } else { - let result = new _URI(data); - result._fsPath = (data).fsPath; + const result = new _URI(data); result._formatted = (data).external; + result._fsPath = (data)._sep === _pathSepMarker ? (data).fsPath : null; return result; } } @@ -343,18 +412,22 @@ export namespace vscUri { interface UriState extends UriComponents { $mid: number; - fsPath: string; external: string; + fsPath: string; + _sep: 1 | undefined; } + const _pathSepMarker = isWindows ? 1 : undefined; // tslint:disable-next-line:class-name class _URI extends URI { - // @ts-ignore - _formatted: string = null; - // @ts-ignore - _fsPath: string = null; + _formatted: string | null = null; + _fsPath: string | null = null; + constructor(schemeOrData: string | UriComponents, authority?: string, path?: string, query?: string, fragment?: string, _strict: boolean = false) { + super(schemeOrData as any, authority, path, query, fragment, _strict); + this._fsPath = this.fsPath; + } get fsPath(): string { if (!this._fsPath) { this._fsPath = _makeFsPath(this); @@ -362,7 +435,7 @@ export namespace vscUri { return this._fsPath; } - public toString(skipEncoding: boolean = false): string { + toString(skipEncoding: boolean = false): string { if (!skipEncoding) { if (!this._formatted) { this._formatted = _asFormatted(this, false); @@ -373,27 +446,166 @@ export namespace vscUri { return _asFormatted(this, true); } } + + toJSON(): UriComponents { + const res = { + $mid: 1 + }; + // cached state + if (this._fsPath) { + res.fsPath = this._fsPath; + if (_pathSepMarker){ + res._sep = _pathSepMarker; + } + } + if (this._formatted) { + res.external = this._formatted; + } + // uri components + if (this.path) { + res.path = this.path; + } + if (this.scheme) { + res.scheme = this.scheme; + } + if (this.authority) { + res.authority = this.authority; + } + if (this.query) { + res.query = this.query; + } + if (this.fragment) { + res.fragment = this.fragment; + } + return res; + } + } + + // reserved characters: https://tools.ietf.org/html/rfc3986#section-2.2 + const encodeTable: { [ch: number]: string } = { + [CharCode.Colon]: '%3A', // gen-delims + [CharCode.Slash]: '%2F', + [CharCode.QuestionMark]: '%3F', + [CharCode.Hash]: '%23', + [CharCode.OpenSquareBracket]: '%5B', + [CharCode.CloseSquareBracket]: '%5D', + [CharCode.AtSign]: '%40', + + [CharCode.ExclamationMark]: '%21', // sub-delims + [CharCode.DollarSign]: '%24', + [CharCode.Ampersand]: '%26', + [CharCode.SingleQuote]: '%27', + [CharCode.OpenParen]: '%28', + [CharCode.CloseParen]: '%29', + [CharCode.Asterisk]: '%2A', + [CharCode.Plus]: '%2B', + [CharCode.Comma]: '%2C', + [CharCode.Semicolon]: '%3B', + [CharCode.Equals]: '%3D', + + [CharCode.Space]: '%20', + }; + + function encodeURIComponentFast(uriComponent: string, allowSlash: boolean): string { + let res: string | undefined = undefined; + let nativeEncodePos = -1; + + for (let pos = 0; pos < uriComponent.length; pos++) { + const code = uriComponent.charCodeAt(pos); + + // unreserved characters: https://tools.ietf.org/html/rfc3986#section-2.3 + if ( + (code >= CharCode.a && code <= CharCode.z) + || (code >= CharCode.A && code <= CharCode.Z) + || (code >= CharCode.Digit0 && code <= CharCode.Digit9) + || code === CharCode.Dash + || code === CharCode.Period + || code === CharCode.Underline + || code === CharCode.Tilde + || (allowSlash && code === CharCode.Slash) + ) { + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } + // check if we write into a new string (by default we try to return the param) + if (res !== undefined) { + res += uriComponent.charAt(pos); + } + + } else { + // encoding needed, we need to allocate a new string + if (res === undefined) { + res = uriComponent.substr(0, pos); + } + + // check with default table first + const escaped = encodeTable[code]; + if (escaped !== undefined) { + + // check if we are delaying native encode + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos, pos)); + nativeEncodePos = -1; + } + + // append escaped variant to result + res += escaped; + + } else if (nativeEncodePos === -1) { + // use native encode only when needed + nativeEncodePos = pos; + } + } + } + + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos)); + } + + return res !== undefined ? res : uriComponent; } + function encodeURIComponentMinimal(path: string): string { + let res: string | undefined = undefined; + for (let pos = 0; pos < path.length; pos++) { + const code = path.charCodeAt(pos); + if (code === CharCode.Hash || code === CharCode.QuestionMark) { + if (res === undefined) { + res = path.substr(0, pos); + } + res += encodeTable[code]; + } else { + if (res !== undefined) { + res += path[pos]; + } + } + } + return res !== undefined ? res : path; + } /** * Compute `fsPath` for the given uri - * @param uri */ function _makeFsPath(uri: URI): string { let value: string; - if (uri.authority && uri.path && uri.scheme === 'file') { + if (uri.authority && uri.path.length > 1 && uri.scheme === 'file') { // unc path: file://shares/c$/far/boo value = `//${uri.authority}${uri.path}`; - } else if (_driveLetterPath.test(uri.path)) { + } else if ( + uri.path.charCodeAt(0) === CharCode.Slash + && (uri.path.charCodeAt(1) >= CharCode.A && uri.path.charCodeAt(1) <= CharCode.Z || uri.path.charCodeAt(1) >= CharCode.a && uri.path.charCodeAt(1) <= CharCode.z) + && uri.path.charCodeAt(2) === CharCode.Colon + ) { // windows drive letter: file:///c:/far/boo value = uri.path[1].toLowerCase() + uri.path.substr(2); } else { // other path value = uri.path; } - if (platform.isWindows) { + if (isWindows) { value = value.replace(/\//g, '\\'); } return value; @@ -405,72 +617,70 @@ export namespace vscUri { function _asFormatted(uri: URI, skipEncoding: boolean): string { const encoder = !skipEncoding - ? encodeURIComponent2 - : encodeNoop; - - const parts: string[] = []; + ? encodeURIComponentFast + : encodeURIComponentMinimal; + let res = ''; let { scheme, authority, path, query, fragment } = uri; if (scheme) { - parts.push(scheme, ':'); + res += scheme; + res += ':'; } if (authority || scheme === 'file') { - parts.push('//'); + res += _slash; + res += _slash; } if (authority) { let idx = authority.indexOf('@'); if (idx !== -1) { + // @ const userinfo = authority.substr(0, idx); authority = authority.substr(idx + 1); idx = userinfo.indexOf(':'); if (idx === -1) { - parts.push(encoder(userinfo)); + res += encoder(userinfo, false); } else { - parts.push(encoder(userinfo.substr(0, idx)), ':', encoder(userinfo.substr(idx + 1))); + // :@ + res += encoder(userinfo.substr(0, idx), false); + res += ':'; + res += encoder(userinfo.substr(idx + 1), false); } - parts.push('@'); + res += '@'; } authority = authority.toLowerCase(); idx = authority.indexOf(':'); if (idx === -1) { - parts.push(encoder(authority)); + res += encoder(authority, false); } else { - parts.push(encoder(authority.substr(0, idx)), authority.substr(idx)); + // : + res += encoder(authority.substr(0, idx), false); + res += authority.substr(idx); } } if (path) { // lower-case windows drive letters in /C:/fff or C:/fff - const m = _upperCaseDrive.exec(path); - if (m) { - if (m[1]) { - path = '/' + m[2].toLowerCase() + path.substr(3); // "/c:".length === 3 - } else { - path = m[2].toLowerCase() + path.substr(2); // // "c:".length === 2 + if (path.length >= 3 && path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(2) === CharCode.Colon) { + const code = path.charCodeAt(1); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3 } - } - - // encode every segement but not slashes - // make sure that # and ? are always encoded - // when occurring in paths - otherwise the result - // cannot be parsed back again - let lastIdx = 0; - while (true) { - let idx = path.indexOf(_slash, lastIdx); - if (idx === -1) { - parts.push(encoder(path.substring(lastIdx))); - break; + } else if (path.length >= 2 && path.charCodeAt(1) === CharCode.Colon) { + const code = path.charCodeAt(0); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `${String.fromCharCode(code + 32)}:${path.substr(2)}`; // "/c:".length === 3 } - parts.push(encoder(path.substring(lastIdx, idx)), _slash); - lastIdx = idx + 1; } + // encode the rest of the path + res += encoder(path, true); } if (query) { - parts.push('?', encoder(query)); + res += '?'; + res += encoder(query, false); } if (fragment) { - parts.push('#', encoder(fragment)); + res += '#'; + res += !skipEncoding ? encodeURIComponentFast(fragment, false) : fragment; } - - return parts.join(_empty); + return res; } } diff --git a/src/test/testing/codeLenses/testFiles.unit.test.ts b/src/test/testing/codeLenses/testFiles.unit.test.ts index ce262c9d3d4b..0c7d4c8e1561 100644 --- a/src/test/testing/codeLenses/testFiles.unit.test.ts +++ b/src/test/testing/codeLenses/testFiles.unit.test.ts @@ -126,29 +126,29 @@ suite('Code lenses - Test files', () => { const workspaceUri = Uri.file('path/to/workspace'); const workspace = { uri: workspaceUri }; const testFile2 = { - fullPath: 'path/to/document2' + fullPath: Uri.file('path/to/document2').fsPath }; const tests = { testFiles: [ { - fullPath: 'path/to/document1' + fullPath: Uri.file('path/to/document1').fsPath }, testFile2 ] }; workspaceService - .setup(w => w.getWorkspaceFolder(document.uri)) + .setup(w => w.getWorkspaceFolder(typemoq.It.isValue(document.uri))) .returns(() => workspace as any) .verifiable(typemoq.Times.once()); testCollectionStorage - .setup(w => w.getTests(workspaceUri)) + .setup(w => w.getTests(typemoq.It.isValue(workspaceUri))) .returns(() => tests as any) .verifiable(typemoq.Times.once()); fileSystem - .setup(f => f.arePathsSame('path/to/document1', 'path/to/document2')) + .setup(f => f.arePathsSame(Uri.file('/path/to/document1').fsPath, Uri.file('/path/to/document2').fsPath)) .returns(() => false); fileSystem - .setup(f => f.arePathsSame('path/to/document2', 'path/to/document2')) + .setup(f => f.arePathsSame(Uri.file('/path/to/document2').fsPath, Uri.file('/path/to/document2').fsPath)) .returns(() => true); const files = codeLensProvider.getTestFileWhichNeedsCodeLens(document as any); assert.deepEqual(files, testFile2 as any); diff --git a/src/test/testing/pytest/services/discoveryService.unit.test.ts b/src/test/testing/pytest/services/discoveryService.unit.test.ts index 187a8ce2428d..b768aee529f6 100644 --- a/src/test/testing/pytest/services/discoveryService.unit.test.ts +++ b/src/test/testing/pytest/services/discoveryService.unit.test.ts @@ -45,7 +45,7 @@ suite('Unit Tests - PyTest - Discovery', () => { test('Ensure discovery is invoked when there are no test directories', async () => { const options: TestDiscoveryOptions = { args: ['some args'], - cwd: __dirname, + cwd: Uri.file(__dirname).fsPath, ignoreCache: true, outChannel: new MockOutputChannel('Tests'), token: new CancellationTokenSource().token, @@ -64,7 +64,7 @@ suite('Unit Tests - PyTest - Discovery', () => { test('Ensure discovery is invoked when there are multiple test directories', async () => { const options: TestDiscoveryOptions = { args: ['some args'], - cwd: __dirname, + cwd: Uri.file(__dirname).fsPath, ignoreCache: true, outChannel: new MockOutputChannel('Tests'), token: new CancellationTokenSource().token, @@ -94,7 +94,7 @@ suite('Unit Tests - PyTest - Discovery', () => { test('Build collection arguments', async () => { const options: TestDiscoveryOptions = { args: ['some args', 'and some more'], - cwd: __dirname, + cwd: Uri.file(__dirname).fsPath, ignoreCache: false, outChannel: new MockOutputChannel('Tests'), token: new CancellationTokenSource().token, @@ -103,7 +103,7 @@ suite('Unit Tests - PyTest - Discovery', () => { const filteredArgs = options.args; const expectedArgs = [ - '--rootdir', __dirname, + '--rootdir', Uri.file(__dirname).fsPath, '-s', ...filteredArgs ]; @@ -117,7 +117,7 @@ suite('Unit Tests - PyTest - Discovery', () => { test('Build collection arguments with ignore in args', async () => { const options: TestDiscoveryOptions = { args: ['some args', 'and some more', '--cache-clear'], - cwd: __dirname, + cwd: Uri.file(__dirname).fsPath, ignoreCache: true, outChannel: new MockOutputChannel('Tests'), token: new CancellationTokenSource().token, @@ -126,7 +126,7 @@ suite('Unit Tests - PyTest - Discovery', () => { const filteredArgs = options.args; const expectedArgs = [ - '--rootdir', __dirname, + '--rootdir', Uri.file(__dirname).fsPath, '-s', ...filteredArgs ]; @@ -140,7 +140,7 @@ suite('Unit Tests - PyTest - Discovery', () => { test('Build collection arguments (& ignore)', async () => { const options: TestDiscoveryOptions = { args: ['some args', 'and some more'], - cwd: __dirname, + cwd: Uri.file(__dirname).fsPath, ignoreCache: true, outChannel: new MockOutputChannel('Tests'), token: new CancellationTokenSource().token, @@ -149,7 +149,7 @@ suite('Unit Tests - PyTest - Discovery', () => { const filteredArgs = options.args; const expectedArgs = [ - '--rootdir', __dirname, + '--rootdir', Uri.file(__dirname).fsPath, '-s', '--cache-clear', ...filteredArgs @@ -164,7 +164,7 @@ suite('Unit Tests - PyTest - Discovery', () => { test('Discover using common discovery', async () => { const options: TestDiscoveryOptions = { args: ['some args', 'and some more'], - cwd: __dirname, + cwd: Uri.file(__dirname).fsPath, ignoreCache: true, outChannel: new MockOutputChannel('Tests'), token: new CancellationTokenSource().token, diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index 9ce26494d6c0..fb8b870c919f 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -55,7 +55,7 @@ mockedVSCode.CancellationTokenSource = vscodeMocks.vscMock.CancellationTokenSour mockedVSCode.CompletionItemKind = vscodeMocks.vscMock.CompletionItemKind; mockedVSCode.SymbolKind = vscodeMocks.vscMock.SymbolKind; mockedVSCode.IndentAction = vscodeMocks.vscMock.IndentAction; -mockedVSCode.Uri = vscodeMocks.vscMock.Uri as any; +mockedVSCode.Uri = vscodeMocks.vscUri.URI as any; mockedVSCode.Range = vscodeMocks.vscMockExtHostedTypes.Range; mockedVSCode.Position = vscodeMocks.vscMockExtHostedTypes.Position; mockedVSCode.Selection = vscodeMocks.vscMockExtHostedTypes.Selection;