import * as vscode from 'vscode' ; import * as path from 'path' ; import regex from './defs_regex' ; export const enum ProcessorResult { DEFINITION , HOVER , SYMBOL , SYMBOL_FULL } export interface SymbolDescriptorExt { kind : vscode . SymbolKind ; declaration : string ; path : string [ ] ; location : vscode . Location ; line : number ; documentation ? : string ; localLabel : boolean ; labelPart ? : string ; labelFull ? : string ; context ? : vscode . TextDocument ; } export class SymbolDescriptor implements SymbolDescriptorExt { constructor ( public declaration , public path , public location , public line = location . range . start . line , public documentation ? , public kind = vscode . SymbolKind . Variable , public localLabel = false ) { } } class IncludeDescriptor { constructor ( public declaration , public labelPath , public fullPath , public location , public line = location . range . start . line ) { } } class FileTable { includes : IncludeDescriptor [ ] = [ ] ; symbols : SymbolDescriptor [ ] = [ ] ; } type SymbolProcessorMultitype = vscode . Definition | vscode . Hover | SymbolDescriptorExt ; export type SymbolMap = { [ name ] : SymbolDescriptor ; } ; export class SymbolProcessor { private _watcher : vscode . FileSystemWatcher ; files : { [ name ] : FileTable ; } = { } ; constructor ( public settings ) { const fileUriHandler = ( ( uri ) => { vscode . workspace . openTextDocument ( uri ) . then ( doc => this . _document ( doc ) ) ; } ) ; vscode . workspace . findFiles ( settings . files . include , settings . files . exclude ) . then ( files => files . forEach ( fileUriHandler ) ) ; vscode . workspace . onDidChangeTextDocument ( ( event ) => this . _document ( event . document ) ) ; this . _watcher = vscode . workspace . createFileSystemWatcher ( settings . files . include ) ; this . _watcher . onDidChange ( fileUriHandler ) ; this . _watcher . onDidCreate ( fileUriHandler ) ; this . _watcher . onDidDelete ( ( uri ) => { delete this . files [ uri . fsPath ] ; } ) ; } destroy ( ) { this . _watcher . dispose ( ) ; } param fsPath param output param prependPath param searched private async _seekSymbols ( fsPath , output , prependPath = [ ] , searched = [ ] ) { let table = this . files [ fsPath ] ; if ( ! table ) { try { const doc = await vscode . workspace . openTextDocument ( fsPath ) ; this . _document ( doc ) ; table = this . files [ fsPath ] ; } catch ( e ) { searched . push ( fsPath ) ; return ; } } searched . push ( fsPath ) ; for ( const symbol of table . symbols ) { const fullPath = [ ... prependPath , ... symbol . path ] ; for ( let i = fullPath . length - 1 ; i >= 0 ; i -- ) { const name = fullPath . slice ( i ) . join ( '.' ) . replace ( '..' , '.' ) ; if ( ! ( name in output ) ) { output [ name ] = symbol ; } } } await Promise . all ( table . includes . map ( async ( include ) => { if ( searched . indexOf ( include . fullPath ) < 0 ) { await this . _seekSymbols ( include . fullPath , output , include . labelPath , searched ) ; } } ) ) ; } param context param declaration param line private _getInclude ( context , declaration , line ) { const fsPath = context . uri . fsPath ; const table = this . files [ fsPath ] ; if ( table ) { const include = table . includes . find ( i => ( i . declaration === declaration && i . line === line ) ) ; if ( include ) { const doc = vscode . Uri . file ( include . fullPath ) ; const range = new vscode . Range ( 0 , 0 , 0 , 0 ) ; return { ... include , destLocation : new vscode . Location ( doc , range ) } ; } } return null ; } param { vscode . TextDocument } doc filesWithIncludes ( context ) { const fsPath = context . uri . fsPath ; const table = this . files [ fsPath ] ; const result = { } ; const putAllToResults = ( t ) : any => t ?. includes . filter ( i => ( ! result [ i . fullPath ] ) && ( result [ i . fullPath ] = i ) && true ) . forEach ( i => putAllToResults ( this . files [ i . fullPath ] ) ) ; if ( table ) { result [ fsPath ] = < any > { declaration : context . fileName , fullPath : fsPath , labelPath : [ ] } ; putAllToResults ( table ) ; } return result ; } param context async symbols ( context ) { const output = { } ; const searched = [ ] ; await this . _seekSymbols ( context . uri . fsPath , output , [ ] , searched ) ; if ( this . settings . seekSymbolsThroughWorkspace ) { for ( const filepath in this . files ) { if ( searched . length && ! searched . includes ( filepath ) ) { await this . _seekSymbols ( filepath , output , [ ] , searched ) ; } } } return output ; } param fileFilter param query param token returns async provideSymbols ( fileFilter , query , token ) { const symbols = { } ; const searched = [ ] ; const output = [ ] ; if ( fileFilter ) { await this . _seekSymbols ( fileFilter , symbols , [ ] , searched ) ; } else { for ( const filepath in this . files ) { if ( searched . length && ! searched . includes ( filepath ) && ! token . isCancellationRequested ) { await this . _seekSymbols ( filepath , symbols , [ ] , searched ) ; } } } if ( token . isCancellationRequested ) { return [ ] ; } const alreadyProcessedDeclarations = [ ] ; Object . values ( symbols ) . reverse ( ) . forEach ( symbol => { if ( ! alreadyProcessedDeclarations . includes ( symbol . declaration ) && ( ! query || symbol . declaration . includes ( query ) ) && ( ! fileFilter || symbol . location . uri . fsPath . endsWith ( fileFilter ) ) ) { output . push ( new vscode . SymbolInformation ( symbol . declaration , symbol . kind , '' , symbol . location ) ) ; alreadyProcessedDeclarations . push ( symbol . declaration ) ; } } ) ; return output ; } param context param position param token param hoverDocumentation returns getFullSymbolAtDocPosition < T = SymbolProcessorMultitype > ( context , position , token , resultType = ProcessorResult . DEFINITION ) { return ( async ( ) => { const lineText = context . lineAt ( position . line ) . text ; const includeLineMatch = regex . includeLine . exec ( lineText ) ; if ( resultType < ProcessorResult . SYMBOL && includeLineMatch ) { const include = this . _getInclude ( context , includeLineMatch [ 4 ] , position . line ) ; if ( include ) { if ( position . character >= include . location . range . start . character && position . character <= include . location . range . end . character ) { if ( resultType === ProcessorResult . HOVER ) { return new vscode . Hover ( include . fullPath , include . location . range ) ; } else { return include . destLocation ; } } } else { return ; } } const commentLineMatch = regex . endComment . exec ( lineText ) ; if ( commentLineMatch && position . character >= commentLineMatch . index ) { return ; } let range = context . getWordRangeAtPosition ( position , regex . stringBounds ) ; if ( range && ! range . isEmpty ) { return ; } range = context . getWordRangeAtPosition ( position , regex . numerals ) ; if ( range && ! range . isEmpty ) { return ; } if ( resultType === ProcessorResult . SYMBOL ) { range = context . getWordRangeAtPosition ( position ) ; } else { range = context . getWordRangeAtPosition ( position , regex . fullLabel ) ; } if ( ! range || ( range && ( range . isEmpty || ! range . isSingleLine ) ) ) { return ; } if ( resultType < ProcessorResult . SYMBOL ) { const activeLinePart = lineText . substr ( 0 , range . end . character ) ; const keywordMatch = regex . shouldSuggestInstruction . exec ( activeLinePart ) ; const arg1RegisterMatch = regex . shouldSuggest1ArgRegister . exec ( activeLinePart ) ; const arg2RegisterMatch = regex . shouldSuggest2ArgRegister . exec ( activeLinePart ) ; const condFlagsMatch = regex . condFlags . exec ( activeLinePart ) ; if ( ( condFlagsMatch && condFlagsMatch [ 2 ] ) || ( keywordMatch && keywordMatch [ 4 ] && regex . keyword . test ( keywordMatch [ 4 ] ) ) || ( arg2RegisterMatch && arg2RegisterMatch [ 3 ] && regex . registers . test ( arg2RegisterMatch [ 3 ] ) ) || ( arg1RegisterMatch && arg1RegisterMatch [ 5 ] && regex . registers . test ( arg1RegisterMatch [ 5 ] ) ) ) { return ; } } const symbols = await this . symbols ( context ) ; if ( token . isCancellationRequested ) { return ; } let lbPart = context . getText ( range ) ; let lbFull = lbPart ; let lbParent = undefined ; let lbModule = undefined ; if ( resultType === ProcessorResult . SYMBOL_FULL && lbPart [ 0 ] !== '.' ) { const lbSplitted = lbFull . split ( '.' ) ; if ( lbSplitted . length >= 2 ) { const testLb = symbols [ lbSplitted [ 0 ] ] ; if ( testLb ) { if ( testLb . kind === vscode . SymbolKind . Module ) { lbModule = lbSplitted . shift ( ) ; if ( lbSplitted . length > 1 ) { lbParent = lbSplitted . shift ( ) ; } lbFull = lbPart = lbSplitted [ 0 ] ; } else { lbParent = lbSplitted . shift ( ) ; lbFull = lbPart = `.${ lbSplitted [ 0 ] }` ; } } } } if ( ! lbParent && ! lbModule ) { for ( let lineNumber = position . line - 1 ; lineNumber >= 0 ; lineNumber -- ) { const line = context . lineAt ( lineNumber ) ; if ( line . isEmptyOrWhitespace ) { continue ; } if ( lbPart [ 0 ] === '.' && ! lbParent ) { const parentLabelMatch = line . text . match ( regex . parentLabel ) ; if ( parentLabelMatch ) { lbParent = parentLabelMatch [ 1 ] ; } } const moduleLineMatch = line . text . match ( regex . moduleLine ) ; if ( moduleLineMatch ) { lbModule = moduleLineMatch [ 2 ] ; break ; } else if ( regex . endmoduleLine . test ( line . text ) ) { break ; } } } lbFull = this . _enlargeLabel ( this . _enlargeLabel ( lbFull , lbParent ) , lbModule ) ; const symbol = symbols [ lbFull ] || symbols [ lbPart ] ; if ( ! symbol ) { return ; } if ( resultType === ProcessorResult . HOVER ) { return new vscode . Hover ( new vscode . MarkdownString ( symbol . documentation ) , range ) ; } else if ( resultType === ProcessorResult . DEFINITION ) { return symbol . location ; } else if ( resultType >= ProcessorResult . SYMBOL ) { symbol . context = context ; symbol . labelPart = lbPart ; symbol . labelFull = lbFull ; return symbol ; } } ) ( ) ; } private _enlargeLabel ( base , prepend ? ) { if ( prepend && base . indexOf ( prepend ) < 0 ) { if ( base [ 0 ] === '.' ) { base = prepend + base ; } else { base = `${ prepend }.${ base }` ; } } return base ; } private _document ( document ) { const table = new FileTable ( ) ; this . files [ document . uri . fsPath ] = table ; const moduleStack = [ ] ; let commentBuffer = [ ] ; let lastFullLabel = null ; for ( let lineNumber = 0 ; lineNumber < document . lineCount ; lineNumber ++ ) { const line = document . lineAt ( lineNumber ) ; if ( line . isEmptyOrWhitespace ) { continue ; } const commentLineMatch = regex . commentLine . exec ( line . text ) ; if ( commentLineMatch ) { const baseLine = commentLineMatch [ 1 ] . trim ( ) ; if ( regex . horizontalRule . test ( baseLine ) ) { continue ; } commentBuffer . push ( baseLine ) ; } else { const includeLineMatch = regex . includeLine . exec ( line . text ) ; const moduleLineMatch = regex . moduleLine . exec ( line . text ) ; const macroLineMatch = regex . macroLine . exec ( line . text ) ; const labelMatch = regex . labelDefinition . exec ( line . text ) ; const labelPath = [ ] ; if ( labelMatch && ! parseInt ( labelMatch [ 1 ] ) ) { labelPath . push ( labelMatch [ 1 ] ) ; let localLabel = false ; let declaration = labelMatch [ 1 ] ; if ( declaration [ 0 ] === '.' ) { localLabel = true ; if ( lastFullLabel ) { labelPath . unshift ( lastFullLabel ) ; declaration = lastFullLabel + declaration ; } } else if ( declaration . indexOf ( '$$' ) < 0 ) { lastFullLabel = declaration ; } if ( moduleStack . length && labelMatch [ 0 ] [ 0 ] !== '@' ) { labelPath . unshift ( moduleStack [ 0 ] ) ; declaration = `${ moduleStack [ 0 ] }.${ declaration }` ; } const location = new vscode . Location ( document . uri , line . range . start ) ; const defineExpressionMatch = regex . defineExpression . exec ( line . text ) ; const evalExpressionMatch = regex . evalExpression . exec ( line . text ) ; const endCommentMatch = regex . endComment . exec ( line . text ) ; if ( defineExpressionMatch ) { const instruction = ( defineExpressionMatch [ 2 ] + ' ' . repeat ( 8 ) ) . substr ( 0 , 8 ) ; commentBuffer . push ( '\n```\n' + instruction + defineExpressionMatch [ 3 ] . trim ( ) + '\n```' ) ; } else if ( evalExpressionMatch ) { commentBuffer . push ( '\n`' + evalExpressionMatch [ 3 ] . trim ( ) + '`' ) ; } if ( endCommentMatch ) { commentBuffer . unshift ( endCommentMatch [ 1 ] . trim ( ) ) ; } table . symbols . push ( new SymbolDescriptor ( declaration , labelPath , location , lineNumber , commentBuffer . join ( '\n' ) . trim ( ) || undefined , vscode . SymbolKind . Variable , localLabel ) ) ; } const pathFragment = [ ... moduleStack . slice ( 0 , 1 ) , ... labelPath . slice ( - 1 ) ] ; if ( includeLineMatch ) { const filename = includeLineMatch [ 4 ] ; const documentDirname = path . dirname ( document . uri . fsPath ) ; const includeName = path . join ( documentDirname , filename ) ; let pos = includeLineMatch . index + includeLineMatch [ 1 ] . length ; const linePos1 = new vscode . Position ( lineNumber , pos ) ; pos += includeLineMatch [ 2 ] . length ; const linePos2 = new vscode . Position ( lineNumber , pos ) ; const range = new vscode . Range ( linePos1 , linePos2 ) ; const location = new vscode . Location ( document . uri , range ) ; table . includes . push ( new IncludeDescriptor ( filename , pathFragment , includeName , location , lineNumber ) ) ; } else if ( macroLineMatch ) { const declaration = macroLineMatch [ 2 ] ; const start = line . firstNonWhitespaceCharacterIndex + macroLineMatch [ 1 ] . length ; const location = new vscode . Location ( document . uri , new vscode . Range ( line . lineNumber , start , line . lineNumber , start + declaration . length ) ) ; const endCommentMatch = regex . endComment . exec ( line . text ) ; if ( endCommentMatch ) { commentBuffer . unshift ( endCommentMatch [ 1 ] . trim ( ) ) ; } if ( macroLineMatch [ 3 ] ) { commentBuffer . unshift ( '\`' + macroLineMatch [ 3 ] . trim ( ) + '`\n' ) ; } commentBuffer . unshift ( `**macro ${ declaration }**` ) ; pathFragment . push ( declaration ) ; table . symbols . push ( new SymbolDescriptor ( declaration , pathFragment , location , lineNumber , commentBuffer . join ( '\n' ) . trim ( ) , vscode . SymbolKind . Function ) ) ; } else if ( moduleLineMatch ) { const declaration = moduleLineMatch [ 2 ] ; const start = line . firstNonWhitespaceCharacterIndex + moduleLineMatch [ 1 ] . length ; const location = new vscode . Location ( document . uri , new vscode . Range ( line . lineNumber , start , line . lineNumber , start + declaration . length ) ) ; moduleStack . unshift ( declaration ) ; commentBuffer . unshift ( `**module ${ declaration }**\n` ) ; pathFragment . push ( declaration ) ; table . symbols . push ( new SymbolDescriptor ( declaration , pathFragment , location , lineNumber , commentBuffer . join ( '\n' ) . trim ( ) , vscode . SymbolKind . Module ) ) ; } else if ( regex . endmoduleLine . test ( line . text ) ) { moduleStack . shift ( ) ; } commentBuffer = [ ] ; } } } }