diff --git a/src/preview/server.ts b/src/preview/server.ts index 4d63a2832..e5642afca 100644 --- a/src/preview/server.ts +++ b/src/preview/server.ts @@ -73,7 +73,7 @@ function getPort(): number { async function getUrl(pdfUri?: vscode.Uri): Promise<{url: string, uri: vscode.Uri}> { // viewer/viewer.js automatically requests the file to server.ts, and server.ts decodes the encoded path of PDF file. - const origUrl = await vscode.env.asExternalUri(vscode.Uri.parse(`http://127.0.0.1:${lw.server.getPort()}`, true)) + const origUrl = await vscode.env.asExternalUri(vscode.Uri.parse(`http://127.0.0.1:${getPort()}`, true)) const url = (origUrl.toString().endsWith('/') ? origUrl.toString().slice(0, -1) : origUrl.toString()) + (pdfUri ? ('/viewer.html?file=' + encodePathWithPrefix(pdfUri)) : '') @@ -166,7 +166,7 @@ function checkHttpOrigin(req: http.IncomingMessage, response: http.ServerRespons } } -function sendOkResponse(response: http.ServerResponse, content: string, contentType: string, cors: boolean = true) { +function sendOkResponse(response: http.ServerResponse, content: Buffer, contentType: string, cors: boolean = true) { // // Headers to enable site isolation. // - https://fetch.spec.whatwg.org/#cross-origin-resource-policy-header @@ -204,8 +204,8 @@ async function handler(request: http.IncomingMessage, response: http.ServerRespo return } try { - const content = await lw.file.read(fileUri, true) - sendOkResponse(response, content ?? '', 'application/pdf') + const content = await vscode.workspace.fs.readFile(fileUri) + sendOkResponse(response, Buffer.from(content), 'application/pdf') logger.log(`Preview PDF file: ${fileUri.toString(true)}`) } catch (e) { logger.logError(`Error reading PDF ${fileUri.toString(true)}`, e) @@ -216,7 +216,7 @@ async function handler(request: http.IncomingMessage, response: http.ServerRespo } if (request.url.endsWith('/config.json')) { const params = lw.viewer.getParams() - sendOkResponse(response, JSON.stringify(params), 'application/json') + sendOkResponse(response, Buffer.from(JSON.stringify(params)), 'application/json') return } let root: string @@ -287,8 +287,8 @@ async function handler(request: http.IncomingMessage, response: http.ServerRespo } } try { - const content = await lw.file.read(fileName, true) - sendOkResponse(response, content ?? '', contentType, false) + const content = await vscode.workspace.fs.readFile(vscode.Uri.file(fileName)) + sendOkResponse(response, Buffer.from(content), contentType, false) } catch (err) { if (typeof (err as any).code === 'string' && (err as any).code === 'FileNotFound') { response.writeHead(404) diff --git a/src/preview/viewer.ts b/src/preview/viewer.ts index 1a05de76d..6234bc9df 100644 --- a/src/preview/viewer.ts +++ b/src/preview/viewer.ts @@ -162,11 +162,7 @@ async function viewInCustomEditor(pdfFile: string): Promise { await vscode.commands.executeCommand('workbench.action.focusRightGroup') } else { await vscode.commands.executeCommand('vscode.openWith', pdfUri, 'latex-workshop-pdf-hook', showOptions) - if (currentColumn === vscode.ViewColumn.One) { - await moveActiveEditor('left', true) - } else { - await vscode.commands.executeCommand('workbench.action.focusRightGroup') - } + await moveActiveEditor('left', true) } } else if (editorGroup === 'right') { const currentColumn = vscode.window.activeTextEditor?.viewColumn @@ -444,6 +440,7 @@ function showInvisibleWebviewPanel(pdfUri: vscode.Uri): boolean { } /** + * !! Test only * Returns the state of the internal PDF viewer of `pdfFilePath`. * * @param pdfUri The path of a PDF file. diff --git a/test/fixtures/unittest/10_viewer_pdf_server/main.pdf b/test/fixtures/unittest/10_viewer_pdf_server/main.pdf new file mode 100644 index 000000000..80bc1e9e0 Binary files /dev/null and b/test/fixtures/unittest/10_viewer_pdf_server/main.pdf differ diff --git a/test/units/09_viewer_server.test.ts b/test/units/09_viewer_server.test.ts index 3c0b0bdb6..df1eb2229 100644 --- a/test/units/09_viewer_server.test.ts +++ b/test/units/09_viewer_server.test.ts @@ -18,6 +18,10 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { await connectWs() }) + after(() => { + sinon.restore() + }) + async function connectWs() { const serverPath = `ws://127.0.0.1:${lw.server.getPort()}` websocket = new ws.WebSocket(serverPath) @@ -27,26 +31,24 @@ describe(path.basename(__filename).split('.')[0] + ':', () => { }) } - async function waitMessage(msg: ClientRequest, timeout = 1000) { - const msgString = JSON.stringify(msg) - let elapsed = 0 - while(true) { - if (handlerStub.called && (handlerStub.lastCall.args?.[1] as Uint8Array).toString() === msgString) { - break - } - await sleep(10) - elapsed += 10 - if (elapsed >= timeout) { - assert.fail(`Timed out waiting for message ${msgString}`) - } - } - } - describe('lw.viewer->server.WsServer', () => { it('should handle websocket messages', async () => { handlerStub.resetHistory() websocket.send(JSON.stringify({ type: 'ping' })) - await waitMessage({ type: 'ping' }) + let elapsed = 0 + while (true) { + if ( + handlerStub.called && + (JSON.parse((handlerStub.lastCall.args?.[1] as Uint8Array).toString()) as ClientRequest).type === 'ping' + ) { + break + } + await sleep(10) + elapsed += 10 + if (elapsed >= 1000) { + assert.fail('Timed out waiting for message "ping"') + } + } }) }) diff --git a/test/units/10_viewer_pdf_server.test.ts b/test/units/10_viewer_pdf_server.test.ts new file mode 100644 index 000000000..6ca73a9cb --- /dev/null +++ b/test/units/10_viewer_pdf_server.test.ts @@ -0,0 +1,229 @@ +import * as vscode from 'vscode' +import * as path from 'path' +import * as sinon from 'sinon' +import { lw } from '../../src/lw' +import { view, viewInWebviewPanel } from '../../src/preview/viewer' +import * as manager from '../../src/preview/viewer/pdfviewermanager' +import { assert, get, mock, set, sleep } from './utils' +import type { ClientRequest } from '../../types/latex-workshop-protocol-types' + +describe.only(path.basename(__filename).split('.')[0] + ':', () => { + const fixture = path.basename(__filename).split('.')[0] + const pdfPath = get.path(fixture, 'main.pdf') + const pdfUri = vscode.Uri.file(pdfPath) + let handlerSpy: sinon.SinonSpy + + before(() => { + mock.init(lw, 'file', 'root', 'server', 'viewer') + handlerSpy = sinon.spy(lw.viewer, 'handler') + }) + + afterEach(async () => { + await vscode.commands.executeCommand('workbench.action.closeAllEditors') + }) + + after(() => { + sinon.restore() + }) + + function waitMessage(type: ClientRequest['type'], timeout = 1000) { + return (async () => { + handlerSpy.resetHistory() + let elapsed = 0 + while (true) { + if ( + handlerSpy.called && + (JSON.parse((handlerSpy.lastCall.args?.[1] as Uint8Array).toString()) as ClientRequest).type === type + ) { + break + } + await sleep(10) + elapsed += 10 + if (elapsed >= timeout) { + assert.fail(`Timed out waiting for message "${type}"`) + } + } + })() + } + + describe('lw.viewer->viewer.viewInCustomEditor', () => { + let execSpy: sinon.SinonSpy + + before(() => { + execSpy = sinon.spy(vscode.commands, 'executeCommand') + }) + + beforeEach(() => { + set.config('viewer.pdf.viewer', 'tab') + execSpy.resetHistory() + }) + + after(() => { + execSpy.restore() + }) + + it('should create a custom editor', async () => { + const promise = waitMessage('loaded') + await view(pdfPath) + await promise + + assert.hasLog(`Open PDF tab for ${pdfUri.toString(true)}`) + }) + + it('should register the created panel in the viewer manager', async () => { + const promise = waitMessage('loaded') + await view(pdfPath) + await promise + + assert.strictEqual(manager.getPanels(pdfUri)?.size, 1) + }) + + it('should create the custom editor at the left group if focused on right', async () => { + set.config('view.pdf.tab.editorGroup', 'left') + mock.activeTextEditor('main.tex', '', { viewColumn: vscode.ViewColumn.Two }) + + await view(pdfPath) + + assert.strictEqual(execSpy.callCount, 2) + assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith') + assert.strictEqual((execSpy.firstCall.args[3] as vscode.TextDocumentShowOptions).viewColumn, 1) + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusRightGroup') + }) + + it('should create the custom editor and move to the left group if focused on left', async () => { + set.config('view.pdf.tab.editorGroup', 'left') + mock.activeTextEditor('main.tex', '', { viewColumn: vscode.ViewColumn.One }) + + await view(pdfPath) + + assert.strictEqual(execSpy.callCount, 3) + assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith') + assert.strictEqual((execSpy.firstCall.args[3] as vscode.TextDocumentShowOptions).viewColumn, -1) + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.moveEditorToLeftGroup') + assert.strictEqual(execSpy.thirdCall.args[0], 'workbench.action.focusRightGroup') + }) + + it('should create the custom editor to the right', async () => { + set.config('view.pdf.tab.editorGroup', 'right') + mock.activeTextEditor('main.tex', '', { viewColumn: vscode.ViewColumn.One }) + + await view(pdfPath) + + assert.strictEqual(execSpy.callCount, 2) + assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith') + assert.strictEqual((execSpy.firstCall.args[3] as vscode.TextDocumentShowOptions).viewColumn, 2) + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusLeftGroup') + }) + + it('should create the custom editor and move to above or below', async () => { + set.config('view.pdf.tab.editorGroup', 'above') + mock.activeTextEditor('main.tex', '', { viewColumn: vscode.ViewColumn.One }) + + await view(pdfPath) + + assert.strictEqual(execSpy.callCount, 3) + assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith') + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.moveEditorToAboveGroup') + assert.strictEqual(execSpy.thirdCall.args[0], 'workbench.action.focusBelowGroup') + + execSpy.resetHistory() + set.config('view.pdf.tab.editorGroup', 'below') + await view(pdfPath) + assert.strictEqual(execSpy.callCount, 3) + assert.strictEqual(execSpy.firstCall.args[0], 'vscode.openWith') + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.moveEditorToBelowGroup') + assert.strictEqual(execSpy.thirdCall.args[0], 'workbench.action.focusAboveGroup') + }) + }) + + describe('lw.viewer->viewer.viewInWebviewPanel', () => { + let execSpy: sinon.SinonSpy + + before(() => { + execSpy = sinon.spy(vscode.commands, 'executeCommand') + }) + + beforeEach(() => { + execSpy.resetHistory() + }) + + after(() => { + execSpy.restore() + }) + + it('should create a webview panel', async () => { + const promise = waitMessage('loaded') + await viewInWebviewPanel(pdfUri, 'current', true) + await promise + + assert.hasLog(`Open PDF tab for ${pdfUri.toString(true)}`) + }) + + it('should register the created panel in the viewer manager', async () => { + const promise = waitMessage('loaded') + await viewInWebviewPanel(pdfUri, 'current', true) + await promise + + assert.strictEqual(manager.getPanels(pdfUri)?.size, 1) + }) + + it('should move the webview panel to the specified editor group', async () => { + const activeEditorStub = mock.activeTextEditor('main.tex', '') + + execSpy.resetHistory() + await viewInWebviewPanel(pdfUri, 'left', true) + assert.strictEqual(execSpy.callCount, 2) + assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToLeftGroup') + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusRightGroup') + + execSpy.resetHistory() + await viewInWebviewPanel(pdfUri, 'right', true) + assert.strictEqual(execSpy.callCount, 2) + assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToRightGroup') + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusLeftGroup') + + execSpy.resetHistory() + await viewInWebviewPanel(pdfUri, 'above', true) + assert.strictEqual(execSpy.callCount, 2) + assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToAboveGroup') + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusBelowGroup') + + execSpy.resetHistory() + await viewInWebviewPanel(pdfUri, 'below', true) + assert.strictEqual(execSpy.callCount, 2) + assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToBelowGroup') + assert.strictEqual(execSpy.secondCall.args[0], 'workbench.action.focusAboveGroup') + + activeEditorStub.restore() + }) + + it('should not move the webview panel if there is no active editor', async () => { + const activeEditorStub = sinon.stub(vscode.window, 'activeTextEditor').value(undefined) + await viewInWebviewPanel(pdfUri, 'left', true) + activeEditorStub.restore() + + assert.strictEqual(execSpy.callCount, 0) + }) + + it('should only move the webview panel but not focus back if `preserveFocus` is `false`', async () => { + const activeEditorStub = mock.activeTextEditor('main.tex', '') + + await viewInWebviewPanel(pdfUri, 'left', false) + + activeEditorStub.restore() + assert.strictEqual(execSpy.callCount, 1) + assert.strictEqual(execSpy.firstCall.args[0], 'workbench.action.moveEditorToLeftGroup') + }) + }) + + describe('lw.viewer->viewer.viewInTab', () => { + it('should create a webview panel', async () => { + set.config('viewer.pdf.viewer', 'legacy') + const promise = waitMessage('loaded') + await view(pdfPath, 'tab') + await promise + + assert.hasLog(`Open PDF tab for ${pdfUri.toString(true)}`) + }) + }) +}) diff --git a/test/units/utils.ts b/test/units/utils.ts index a531e31ab..2dd58d99e 100644 --- a/test/units/utils.ts +++ b/test/units/utils.ts @@ -179,7 +179,7 @@ export const mock = { textDocument: (filePath: string, content: string, params: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string } = {}) => { return sinon.stub(vscode.workspace, 'textDocuments').value([ new TextDocument(filePath, content, params) ]) }, - activeTextEditor: (filePath: string, content: string, params: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string } = {}) => { + activeTextEditor: (filePath: string, content: string, params: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string, viewColumn?: vscode.ViewColumn } = {}) => { return sinon.stub(vscode.window, 'activeTextEditor').value(new TextEditor(filePath, content, params)) } } @@ -255,8 +255,11 @@ class TextEditor implements vscode.TextEditor { options: vscode.TextEditorOptions = {} viewColumn: vscode.ViewColumn | undefined = vscode.ViewColumn.Active - constructor(filePath: string, content: string, { languageId = 'latex', isDirty = false, isClosed = false, scheme = 'file' }: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string }) { + constructor(filePath: string, content: string, { languageId = 'latex', isDirty = false, isClosed = false, scheme = 'file', viewColumn = undefined }: { languageId?: string, isDirty?: boolean, isClosed?: boolean, scheme?: string, viewColumn?: vscode.ViewColumn }) { this.document = new TextDocument(filePath, content, { languageId, isDirty, isClosed, scheme }) + if (viewColumn !== undefined) { + this.viewColumn = viewColumn + } } edit(_: (_: vscode.TextEditorEdit) => void): Thenable { throw new Error('Not implemented.') }