'use strict' ; import * as vscode from 'vscode' ; import { Commands } from './commands' ; import { Flow , Issue , Location , TextRange } from './protocol' ; import { resolveExtensionFile } from './util' ; /**
 * Base decoration type for secondary locations.
 * See contributes.colors in package.json for theme color values.
 */ const SECONDARY_LOCATION_DECORATIONS = vscode . window . createTextEditorDecorationType ( { backgroundColor : new vscode . ThemeColor ( 'sonarlint.locations.background' ) , before : { backgroundColor : new vscode . ThemeColor ( 'sonarlint.locations.indexBackground' ) , color : new vscode . ThemeColor ( 'sonarlint.locations.indexText' ) , margin : '0.2em' } , rangeBehavior : vscode . DecorationRangeBehavior . ClosedClosed } ) ; /**
 * Decoration type for selected secondary location.
 * See contributes.colors in package.json for theme color values.
 */ const SELECTED_SECONDARY_LOCATION_DECORATION = vscode . window . createTextEditorDecorationType ( { backgroundColor : new vscode . ThemeColor ( 'sonarlint.locations.background' ) , before : { backgroundColor : new vscode . ThemeColor ( 'sonarlint.locations.indexSelectedBackground' ) , color : new vscode . ThemeColor ( 'sonarlint.locations.indexSelectedText' ) , fontWeight : 'bold' , margin : '0.2em' } , rangeBehavior : vscode . DecorationRangeBehavior . ClosedClosed } ) ; export class IssueItem extends vscode . TreeItem { readonly children : FlowItem [ ] | LocationItem [ ] ; constructor ( issue ) { const highlightOnly = issue . flows . every ( f => f . locations . every ( l => ! l . message || l . message === '' ) ) ; const collapsibleState = highlightOnly ? vscode . TreeItemCollapsibleState . None : vscode . TreeItemCollapsibleState . Expanded ; super ( issue . message , collapsibleState ) ; this . description = `(${ issue . ruleKey })` ; const severityIcon = resolveExtensionFile ( 'images' , 'severity' , `${ issue . severity . toLowerCase ( ) }.png` ) ; this . iconPath = { light : severityIcon , dark : severityIcon } ; if ( highlightOnly ) { this . children = [ ] ; } else if ( issue . flows . every ( f => f . locations . length === 1 ) ) { this . children = issue . flows . map ( ( f , i ) => new LocationItem ( f . locations [ 0 ] , i + 1 , this ) ) ; } else { this . children = issue . flows . map ( ( f , i ) => new FlowItem ( f , i , this ) ) ; } } } export class FlowItem extends vscode . TreeItem { readonly parent : LocationTreeItem ; readonly children : ( LocationItem | FileItem ) [ ] ; constructor ( flow , index , parent ) { const collapsibleState = index === 0 ? vscode . TreeItemCollapsibleState . Expanded : vscode . TreeItemCollapsibleState . Collapsed ; super ( `Flow ${ index + 1 }` , collapsibleState ) ; const flowLocations = Array . from ( flow . locations ) ; flowLocations . reverse ( ) ; if ( new Set ( flowLocations . map ( l => l . uri ) ) . size > 1 ) { let locationIndex = 0 ; let currentUri = null ; let currentPath = null ; let fileLocations = [ ] ; this . children = [ ] ; while ( locationIndex < flowLocations . length ) { currentUri = flowLocations [ locationIndex ] . uri ; currentPath = flowLocations [ locationIndex ] . filePath ; fileLocations . push ( flowLocations [ locationIndex ] ) ; if ( locationIndex === flowLocations . length - 1 || flowLocations [ locationIndex + 1 ] . uri !== currentUri ) { this . children . push ( new FileItem ( currentUri , currentPath , locationIndex + 1 , fileLocations , this ) ) ; fileLocations = [ ] ; } locationIndex += 1 ; } } else { this . children = flowLocations . map ( ( l , i ) => new LocationItem ( l , i + 1 , this ) ) ; } this . parent = parent ; } } export class FileItem extends vscode . TreeItem { readonly children : LocationItem [ ] ; readonly parent : FlowItem ; constructor ( uri , filePath , lastIndex , locations , parent ) { const label = uri ? uri . substring ( uri . lastIndexOf ( '/' ) + 1 ) : filePath . substring ( filePath . lastIndexOf ( '/' ) + 1 ) ; super ( label , vscode . TreeItemCollapsibleState . Expanded ) ; this . children = locations . map ( ( l , i ) => new LocationItem ( l , lastIndex + 1 - locations . length + i , this ) ) ; this . parent = parent ; this . resourceUri = uri ? vscode . Uri . parse ( uri ) : null ; if ( ! uri ) { this . tooltip = filePath ; } this . iconPath = vscode . ThemeIcon . File ; } } export class LocationItem extends vscode . TreeItem { readonly parent : LocationParentItem ; readonly location : Location ; readonly index : number ; constructor ( location , index , parent ) { super ( `${ index }: ${ location . message }` , vscode . TreeItemCollapsibleState . None ) ; this . index = index ; if ( location . uri ) { if ( location . exists ) { if ( location . codeMatches ) { this . description = `[${ location . textRange . startLine }, ${ location . textRange . startLineOffset }]` ; } else { this . description = '(local code does not match)' ; } this . command = { title : 'Navigate' , command : Commands . NAVIGATE_TO_LOCATION , arguments : [ this ] } ; } else { this . description = '(unreachable in local code)' ; } } else { this . description = '(unreachable in local code)' ; } this . parent = parent ; this . location = location ; } } type ChildItem = FlowItem | FileItem | LocationItem ; type LocationParentItem = IssueItem | FlowItem | FileItem ; export type LocationTreeItem = IssueItem | ChildItem ; export class SecondaryLocationsTree implements vscode . TreeDataProvider < LocationTreeItem > { private readonly _onDidChangeTreeData = new vscode . EventEmitter < LocationTreeItem | undefined > ( ) ; readonly onDidChangeTreeData = this . _onDidChangeTreeData . event ; private rootItem ? : IssueItem ; constructor ( ) { this . rootItem = null ; } async showAllLocations ( issue ) { this . rootItem = new IssueItem ( issue ) ; this . notifyRootChanged ( ) ; if ( this . rootItem . children . length === 0 ) { const uri = issue . fileUri ; const editor = await vscode . window . showTextDocument ( vscode . Uri . parse ( uri ) ) ; const locations = issue . flows . map ( f => f . locations ) . reduce ( ( acc , cur ) => acc . concat ( cur ) , [ ] ) ; editor . setDecorations ( SECONDARY_LOCATION_DECORATIONS , locations . map ( ( l , i ) => buildDecoration ( new LocationItem ( l , i + 1 , this . rootItem ) , editor . document ) ) ) ; } else if ( this . rootItem . children [ 0 ] instanceof LocationItem ) { navigateToLocation ( this . rootItem . children [ 0 ] ) ; } else if ( this . rootItem . children [ 0 ] . children [ 0 ] instanceof LocationItem ) { navigateToLocation ( this . rootItem . children [ 0 ] . children [ 0 ] ) ; } else { navigateToLocation ( this . rootItem . children [ 0 ] . children [ 0 ] . children [ 0 ] ) ; } } hideLocations ( ) { this . rootItem = null ; this . notifyRootChanged ( ) ; vscode . window . visibleTextEditors . forEach ( e => { e . setDecorations ( SECONDARY_LOCATION_DECORATIONS , [ ] ) ; e . setDecorations ( SELECTED_SECONDARY_LOCATION_DECORATION , [ ] ) ; } ) ; } private notifyRootChanged ( ) { this . _onDidChangeTreeData . fire ( undefined ) ; } getTreeItem ( element ) { return element ; } getChildren ( element ? ) { if ( ! element ) { return [ this . rootItem ] ; } else if ( element instanceof IssueItem ) { return element . children ; } else if ( element instanceof FlowItem ) { return element . children ; } else if ( element instanceof FileItem ) { return element . children ; } else { return [ ] ; } } getParent ( element ? ) { if ( element === this . rootItem ) { return null ; } else { return ( element as ChildItem ) . parent ; } } } export async function navigateToLocation ( item ) { const { uri , textRange } = item . location ; const editor = await vscode . window . showTextDocument ( vscode . Uri . parse ( uri ) ) ; const range = vscodeRange ( textRange ) ; if ( isValidRange ( range , editor . document ) ) { editor . selection = new vscode . Selection ( range . start , range . end ) ; editor . revealRange ( range , vscode . TextEditorRevealType . InCenter ) ; } editor . setDecorations ( SELECTED_SECONDARY_LOCATION_DECORATION , buildMainDecorations ( item , editor . document ) ) ; editor . setDecorations ( SECONDARY_LOCATION_DECORATIONS , buildSiblingDecorations ( item , editor . document ) ) ; } function buildMainDecorations ( item , document ) { return [ buildDecoration ( item , document ) ] . filter ( d => d ) ; } function buildSiblingDecorations ( item , document ) { const locationsInSameFile = item . parent instanceof FileItem ? ( item . parent . parent . children as FileItem [ ] ) . filter ( f => hasSameNullSafeResourceUri ( f , item . parent as FileItem ) ) . map ( f => f . children ) . reduce ( ( acc , cur ) => acc . concat ( cur ) , [ ] ) : ( item . parent . children as LocationItem [ ] ) ; return locationsInSameFile . filter ( i => i !== item ) . filter ( i => i . location . textRange ) . map ( i => buildDecoration ( i , document ) ) . filter ( d => d ) ; } function hasSameNullSafeResourceUri ( f1 , f2 ) { return f1 . resourceUri === null && f2 . resourceUri === null || f1 . resourceUri !== null && f2 . resourceUri !== null && f1 . resourceUri . toString ( ) === f2 . resourceUri . toString ( ) ; } function buildDecoration ( item , document ) { const location = item . location ; const range = new vscode . Range ( new vscode . Position ( location . textRange . startLine - 1 , location . textRange . startLineOffset ) , new vscode . Position ( location . textRange . endLine - 1 , location . textRange . endLineOffset ) ) ; if ( ! isValidRange ( range , document ) ) { return null ; } return ( { range , hoverMessage : location . message , renderOptions : { before : { contentText : `\u2000${ item . index }\u2000` } } } ) ; } function vscodeRange ( textRange ) { const startPosition = new vscode . Position ( textRange . startLine - 1 , textRange . startLineOffset ) ; const endPosition = new vscode . Position ( textRange . endLine - 1 , textRange . endLineOffset ) ; return new vscode . Range ( startPosition , endPosition ) ; } function isValidRange ( textRange , document ) { const validatedRange = document . validateRange ( textRange ) ; return textRange . isEqual ( validatedRange ) ; }