import { ILabShell , ILayoutRestorer , JupyterFrontEnd , JupyterFrontEndPlugin } from '@jupyterlab/application' ; import { IWidgetTracker , WidgetTracker , showErrorMessage } from '@jupyterlab/apputils' ; import { CodeEditor } from '@jupyterlab/codeeditor' ; import { PathExt , URLExt } from '@jupyterlab/coreutils' ; import { IDocumentManager } from '@jupyterlab/docmanager' ; import { DocumentRegistry , IDocumentWidget } from '@jupyterlab/docregistry' ; import { FileEditor , IEditorTracker } from '@jupyterlab/fileeditor' ; import { ServerConnection } from '@jupyterlab/services' ; import { ISettingRegistry } from '@jupyterlab/settingregistry' ; import { IStateDB } from '@jupyterlab/statedb' ; import { ReadonlyJSONObject , Token } from '@lumino/coreutils' ; import { DisposableSet } from '@lumino/disposable' ; import { ErrorPanel } from './error' ; import { PDFJSDocumentWidget , PDFJSViewer , PDFJSViewerFactory } from './pdf' ; import { ILauncher } from '@jupyterlab/launcher' ; import { LabIcon } from '@jupyterlab/ui-components' ; import { IFileBrowserFactory } from '@jupyterlab/filebrowser' ; import { ICommandPalette } from '@jupyterlab/apputils' ; import { IMainMenu } from '@jupyterlab/mainmenu' ; import latexIconStr from '../style/latex.svg' ; import '../style/index.css' ; /**
 * A class that tracks editor widgets.
 */ export interface IPDFJSTracker extends IWidgetTracker < IDocumentWidget < PDFJSViewer > > { } /**
 * The editor tracker token.
 */ export const IPDFJSTracker = new Token < IPDFJSTracker > ( '@jupyterlab/latex:IPDFJSTracker' ) ; const latexPluginId = '@jupyterlab/latex:plugin' ; namespace CommandIDs { /**
     * Open a live preview for a `.tex` document.
     */ export const openLatexPreview = 'latex:open-preview' ; /**
     * Reveal in the editor a position from the pdf using SyncTeX.
     */ export const synctexEdit = 'latex:synctex-edit' ; /**
     * Reveal in the pdf a position from the editor using SyncTeX.
     */ export const synctexView = 'latex:synctex-view' ; /**
     * Create new latex file
     */ export const createNew = 'latex:create-new-latex-file' ; } /**
 * The options for a SyncTeX view command,
 * mapping the editor position the PDF.
 */ type ISynctexViewOptions = CodeEditor . IPosition ; /**
 * The options for a SyncTeX edit command,
 * mapping the pdf position to an editor position.
 */ type ISynctexEditOptions = PDFJSViewer . IPosition ; /**
 * The JupyterFrontEnd plugin for the LaTeX extension.
 */ const latexPlugin = { id : latexPluginId , requires : [ IFileBrowserFactory , IDocumentManager , IEditorTracker , ILabShell , ILayoutRestorer , IPDFJSTracker , ISettingRegistry , IStateDB ] , optional : [ ILauncher , IMainMenu , ICommandPalette ] , activate : activateLatexPlugin , autoStart : true } ; param path param settings returns function latexBuildRequest ( path , synctex , settings ) { let fullUrl = URLExt . join ( settings . baseUrl , 'latex' , 'build' , path ) ; fullUrl += `?synctex=${ synctex ? 1 : 0 }` ; return ServerConnection . makeRequest ( fullUrl , { } , settings ) . then ( response => { if ( response . status !== 200 ) { return response . text ( ) . then ( data => { throw new ServerConnection . ResponseError ( response , data ) ; } ) ; } return response . text ( ) ; } ) ; } param path param settings returns function synctexEditRequest ( path , pos , settings ) { let url = URLExt . join ( settings . baseUrl , 'latex' , 'synctex' , path ) ; url += `?page=${ pos . page }&x=${ pos . x }&y=${ pos . y }` ; return ServerConnection . makeRequest ( url , { } , settings ) . then ( response => { if ( response . status !== 200 ) { return response . text ( ) . then ( data => { throw new ServerConnection . ResponseError ( response , data ) ; } ) ; } return response . json ( ) . then ( json => { return { line : parseInt ( json . line , 10 ) , column : parseInt ( json . column , 10 ) } as ISynctexViewOptions ; } ) ; } ) ; } param path param settings returns function synctexViewRequest ( path , pos , settings ) { let url = URLExt . join ( settings . baseUrl , 'latex' , 'synctex' , path ) ; url += `?line=${ pos . line }&column=${ pos . column }` ; return ServerConnection . makeRequest ( url , { } , settings ) . then ( response => { if ( response . status !== 200 ) { return response . text ( ) . then ( data => { throw new ServerConnection . ResponseError ( response , data ) ; } ) ; } return response . json ( ) . then ( json => { return { page : parseInt ( json . page , 10 ) , x : parseFloat ( json . x ) , y : parseFloat ( json . y ) } as ISynctexEditOptions ; } ) ; } ) ; } /**
 * Activate the file browser.
 */ function activateLatexPlugin ( app , browserFactory , manager , editorTracker , shell , restorer , pdfTracker , settingRegistry , state , launcher , menu , palette ) { const { commands } = app ; const id = 'jupyterlab-latex' ; const icon = new LabIcon ( { name : 'launcher:latex-icon' , svgstr : latexIconStr } ) ; let synctex = true ; const serverSettings = ServerConnection . makeSettings ( ) ; const hasWidget = ( ) => ! ! editorTracker . currentWidget ; const openPreview = ( widget ) => { let texContext = manager . contextForWidget ( widget ) ; if ( ! texContext ) { return ; } if ( Private . previews . has ( texContext . path ) ) { texContext . save ( ) ; return ; } const dirName = PathExt . dirname ( texContext . path ) ; const baseName = PathExt . basename ( texContext . path , '.tex' ) ; const pdfFilePath = PathExt . join ( dirName , baseName + '.pdf' ) ; let pdfContext ; let errorPanel = null ; let pending = false ; const findOpenOrRevealPDF = ( ) => { let pdfWidget = manager . findWidget ( pdfFilePath ) ; if ( ! pdfWidget ) { pdfWidget = manager . openOrReveal ( pdfFilePath , 'PDFJS' , undefined , { mode : 'split-right' } ) ; } if ( ! pdfWidget ) { return ; } ( pdfWidget as PDFJSDocumentWidget ) . content . positionRequested . connect ( reverseSearch ) ; pdfContext = manager . contextForWidget ( pdfWidget ) ! ; pdfContext . disposed . connect ( cleanupPreviews ) ; } ; const reverseSearch = ( s , pos ) => { synctexEditRequest ( s . context . path , { ... pos , x : 0 } , serverSettings ) . then ( ( view ) => { const cursor = { line : view . line - 1 , column : 0 } ; ( widget as IDocumentWidget < FileEditor > ) . content . editor . setCursorPosition ( cursor ) ; } ) ; } ; const errorPanelInit = ( err ) => { if ( err . response . status === 404 ) { const noServerExt = { message : 'You probably do not have jupyterlab_latex ' + 'installed or enabled. ' + 'Please, run "pip install -U jupyterlab_latex." ' + 'If that does not work, try "jupyter serverextension ' + 'enable --sys-prefix jupyterlab_latex".' } ; showErrorMessage ( 'Server Extension Error' , noServerExt ) ; return ; } errorPanel = Private . createErrorPanel ( ) ; errorPanel . disposed . connect ( ( ) => { errorPanel = null ; } ) ; shell . add ( errorPanel , 'main' , { ref : widget . id , mode : 'split-bottom' } ) ; errorPanel . text = err . message ; } ; const onFileChanged = ( ) => { if ( pending ) { return Promise . resolve ( void 0 ) ; } pending = true ; return latexBuildRequest ( texContext ! . path , synctex , serverSettings ) . then ( ( ) => { pdfContext ? pdfContext . revert ( ) : findOpenOrRevealPDF ( ) ; if ( errorPanel ) { errorPanel . close ( ) ; } pending = false ; } ) . catch ( err => { if ( ! errorPanel ) { errorPanelInit ( err ) ; } pending = false ; } ) ; } ; texContext . fileChanged . connect ( onFileChanged ) ; onFileChanged ( ) . then ( ( ) => { if ( ! errorPanel ) { findOpenOrRevealPDF ( ) ; } } ) ; const cleanupPreviews = ( ) => { if ( ! texContext ) { return ; } Private . previews . delete ( texContext . path ) ; if ( errorPanel ) { errorPanel . close ( ) ; } texContext . fileChanged . disconnect ( onFileChanged ) ; state . save ( id , { paths : Array . from ( Private . previews ) } ) ; } ; texContext . disposed . connect ( cleanupPreviews ) ; Private . previews . add ( texContext . path ) ; state . save ( id , { paths : Array . from ( Private . previews ) } ) ; } ; Promise . all ( [ state . fetch ( id ) , app . restored ] ) . then ( ( [ args ] ) => { let paths = ( args && ( ( args as ReadonlyJSONObject ) [ 'paths' ] as string [ ] ) ) || [ ] ; paths . forEach ( path => { let widget = manager . findWidget ( path ) ; if ( widget ) { openPreview ( widget ) ; } } ) ; } ) ; Promise . all ( [ settingRegistry . load ( latexPluginId ) , app . restored ] ) . then ( ( [ settings ] ) => { let disposables = new DisposableSet ( ) ; const onSettingsUpdated = ( settings ) => { const val = settings . get ( 'synctex' ) . composite as boolean | null ; synctex = val === true || val === false ? val : true ; disposables . dispose ( ) ; if ( synctex ) { disposables = addSynctexCommands ( app , editorTracker , pdfTracker , serverSettings ) ; } } ; settings . changed . connect ( onSettingsUpdated ) ; onSettingsUpdated ( settings ) ; } ) . catch ( ( reason ) => { console . error ( reason . message ) ; } ) ; commands . addCommand ( CommandIDs . openLatexPreview , { execute : ( ) => { let widget = editorTracker . currentWidget ; if ( widget ) { openPreview ( widget ) ; } } , isEnabled : hasWidget , isVisible : ( ) => { let widget = editorTracker . currentWidget ; return ( ( widget && PathExt . extname ( widget . context . path ) === '.tex' ) || false ) ; } , label : 'Show LaTeX Preview' } ) ; const command = CommandIDs . createNew ; const command_latex_preview = CommandIDs . openLatexPreview ; commands . addCommand ( command , { label : args => ( args [ 'isPalette' ] ? 'New LaTeX File' : 'LaTeX File' ) , caption : 'Create a new LaTeX file' , icon : args => ( args [ 'isPalette' ] ? undefined : icon ) , execute : async ( args ) => { const cwd = args [ 'cwd' ] || browserFactory . defaultBrowser . model . path ; const model = await commands . execute ( 'docmanager:new-untitled' , { path : cwd , type : 'file' , ext : 'tex' } ) ; return commands . execute ( 'docmanager:open' , { path : model . path , factory : FACTORY_EDITOR } ) ; } } ) ; app . contextMenu . addItem ( { command : command_latex_preview , selector : '.jp-FileEditor' } ) ; if ( launcher ) { launcher . add ( { command , category : LAUNCHER_CATEGORY , rank : 1 } ) ; } if ( palette ) { palette . addItem ( { command : command , args : { isPalette : true } , category : PALETTE_CATEGORY } ) ; } if ( menu ) { menu . fileMenu . newMenu . addGroup ( [ { command } ] , 30 ) ; } } /**
 * Add commands, keyboard shortcuts, and menu items for SyncTeX-related things.
 */ function addSynctexCommands ( app , editorTracker , pdfTracker , serverSettings ) { const disposables = new DisposableSet ( ) ; const hasPDFWidget = ( ) => ! ! pdfTracker . currentWidget ; const hasEditorWidget = ( ) => ! ! editorTracker . currentWidget ; disposables . add ( app . commands . addCommand ( CommandIDs . synctexEdit , { execute : ( ) => { let widget = pdfTracker . currentWidget ; if ( widget ) { const pos = widget . content . position ; return synctexEditRequest ( widget . context . path , pos , serverSettings ) . then ( ( view ) => { if ( ! widget ) { return ; } const baseName = PathExt . basename ( widget . context . path , '.pdf' ) ; const dirName = PathExt . dirname ( widget . context . path ) ; const texFilePath = PathExt . join ( dirName , baseName + '.tex' ) ; const editorWidget = editorTracker . find ( editor => editor . context . path === texFilePath ) ; if ( ! editorWidget ) { return ; } editorWidget . content . editor . setCursorPosition ( view ) ; } ) ; } } , isEnabled : hasPDFWidget , isVisible : ( ) => { const widget = pdfTracker . currentWidget ; if ( ! widget ) { return false ; } const baseName = PathExt . basename ( widget . context . path , '.pdf' ) ; const dirName = PathExt . dirname ( widget . context . path ) ; const texFilePath = PathExt . join ( dirName , baseName + '.tex' ) ; return Private . previews . has ( texFilePath ) ; } , label : 'Scroll Editor to Page' } ) ) ; disposables . add ( app . commands . addCommand ( CommandIDs . synctexView , { execute : ( ) => { let widget = editorTracker . currentWidget ; if ( widget ) { let pos = widget . content . editor . getCursorPosition ( ) ; pos = { line : pos . line + 1 , column : pos . column + 1 } ; return synctexViewRequest ( widget . context . path , pos , serverSettings ) . then ( ( edit ) => { if ( ! widget ) { return ; } const baseName = PathExt . basename ( widget . context . path , '.tex' ) ; const dirName = PathExt . dirname ( widget . context . path ) ; const pdfFilePath = PathExt . join ( dirName , baseName + '.pdf' ) ; const pdfWidget = pdfTracker . find ( pdf => pdf . context . path === pdfFilePath ) ; if ( ! pdfWidget ) { return ; } pdfWidget . content . position = { ... edit , x : 0 } ; } ) ; } } , isEnabled : hasEditorWidget , isVisible : ( ) => { let widget = editorTracker . currentWidget ; return ! ! widget && Private . previews . has ( widget . context . path ) ; } , label : 'Scroll PDF to Cursor' } ) ) ; disposables . add ( app . contextMenu . addItem ( { command : CommandIDs . synctexView , selector : '.jp-FileEditor' } ) ) ; disposables . add ( app . contextMenu . addItem ( { command : CommandIDs . synctexEdit , selector : '.jp-PDFJSContainer' } ) ) ; disposables . add ( app . commands . addKeyBinding ( { selector : '.jp-FileEditor' , keys : [ 'Accel Shift X' ] , command : CommandIDs . synctexView } ) ) ; disposables . add ( app . commands . addKeyBinding ( { selector : '.jp-PDFJSContainer' , keys : [ 'Accel Shift X' ] , command : CommandIDs . synctexEdit } ) ) ; return disposables ; } /**
 * The list of file types for pdfs.
 */ const FILE_TYPES = [ 'PDF' ] ; /**
 * The name of the factory that creates pdf widgets.
 */ const FACTORY = 'PDFJS' ; const FACTORY_EDITOR = 'Editor' ; const LAUNCHER_CATEGORY = 'Other' ; const PALETTE_CATEGORY = 'LaTeX Editor' ; /**
 * The pdf file handler extension.
 */ const pdfjsPlugin = { activate : activatePDFJS , id : '@jupyterlab/pdfjs-extension:plugin' , requires : [ ILayoutRestorer ] , provides : IPDFJSTracker , autoStart : true } ; function activatePDFJS ( app , restorer ) { const namespace = 'pdfjs-widget' ; const factory = new PDFJSViewerFactory ( { name : FACTORY , modelName : 'base64' , fileTypes : FILE_TYPES , readOnly : true } ) ; const tracker = new WidgetTracker < IDocumentWidget < PDFJSViewer > > ( { namespace } ) ; restorer . restore ( tracker , { command : 'docmanager:open' , args : widget => ( { path : widget . context . path , factory : FACTORY } ) , name : widget => widget . context . path } ) ; app . docRegistry . addWidgetFactory ( factory ) ; factory . widgetCreated . connect ( ( sender , widget ) => { widget . context . pathChanged . connect ( ( ) => { tracker . save ( widget ) ; } ) ; tracker . add ( widget ) ; const types = app . docRegistry . getFileTypesForPath ( widget . context . path ) ; if ( types . length > 0 ) { widget . title . iconClass = types [ 0 ] . iconClass || '' ; widget . title . iconLabel = types [ 0 ] . iconLabel || '' ; } } ) ; return tracker ; } /**
 * Export the plugins as default.
 */ const plugins = [ latexPlugin , pdfjsPlugin ] ; export default plugins ; /**
 * A namespace for private module data.
 */ namespace Private { /**
     * A counter for unique IDs.
     */ let id = 0 ; /**
     * A cache for the currently active LaTeX previews.
     */ export const previews = new Set < string > ( ) ; /**
     * Create an error panel widget.
     */ export function createErrorPanel ( ) { const errorPanel = new ErrorPanel ( ) ; errorPanel . id = `latex-error-${ ++ id }` ; errorPanel . title . label = 'LaTeX Error' ; errorPanel . title . closable = true ; return errorPanel ; } }