1// Copyright 2024 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15import * as vscode from 'vscode'; 16 17import { 18 EventEmitter, 19 FileDecoration, 20 FileDecorationProvider, 21 ThemeColor, 22 Uri, 23} from 'vscode'; 24 25import { ClangdActiveFilesCache, FileStatus } from './clangd'; 26import { Disposable } from './disposables'; 27 28import { 29 didChangeClangdConfig, 30 didChangeTarget, 31 didUpdateActiveFilesCache, 32} from './events'; 33 34import logger from './logging'; 35import { getPigweedProjectRoot } from './project'; 36import { settings, workingDir } from './settings'; 37 38type DecorationsMap = Map<string, FileDecoration | undefined>; 39 40/** Event emitted when the file decorations have changed. */ 41const didChangeFileDecorations = new EventEmitter<Uri[]>(); 42 43const DECORATION: Record<FileStatus, FileDecoration | undefined> = { 44 ACTIVE: undefined, 45 INACTIVE: { 46 badge: 'X', 47 color: new ThemeColor('disabledForeground'), 48 tooltip: 'This file is not built in the current target group', 49 }, 50 ORPHANED: { 51 badge: '!!', 52 color: new ThemeColor('errorForeground'), 53 tooltip: 'This file is not built by any defined target groups', 54 }, 55}; 56 57export class InactiveFileDecorationProvider 58 extends Disposable 59 implements FileDecorationProvider 60{ 61 private decorations: DecorationsMap; 62 private activeFilesCache: ClangdActiveFilesCache; 63 64 constructor(activeFilesCache: ClangdActiveFilesCache) { 65 super(); 66 this.activeFilesCache = activeFilesCache; 67 this.decorations = new Map(); 68 69 this.disposables.push( 70 ...[ 71 vscode.window.registerFileDecorationProvider(this), 72 didChangeTarget.event(this.update), 73 didUpdateActiveFilesCache.event(() => this.update()), 74 ], 75 ); 76 77 if (!settings.hideInactiveFileIndicators()) { 78 this.disposables.push(didChangeClangdConfig.event(() => this.update())); 79 } 80 } 81 82 // When this event is triggered, VSC is notified that the files included in 83 // the event have had their decorations changed. 84 readonly onDidChangeFileDecorations = didChangeFileDecorations.event; 85 86 // This is called internally by VSC to provide the decoration for a file 87 // whenever it's asked for. 88 provideFileDecoration(uri: Uri) { 89 return this.decorations.get(uri.toString()); 90 } 91 92 /** Provide updated decorations for the current target's active files. */ 93 async refreshDecorations(providedTarget?: string): Promise<DecorationsMap> { 94 const newDecorations: DecorationsMap = new Map(); 95 const target = providedTarget ?? settings.codeAnalysisTarget(); 96 97 if (!target) { 98 return newDecorations; 99 } 100 101 const projectRoot = await getPigweedProjectRoot(settings, workingDir); 102 103 if (!projectRoot) { 104 return newDecorations; 105 } 106 107 const workspaceFiles = await vscode.workspace.findFiles( 108 '**/*.{c,cc,cpp,h,hpp}', // include 109 '**/{.*,bazel*,external}/**', // exclude 110 ); 111 112 for (const uri of workspaceFiles) { 113 const { status } = await this.activeFilesCache.fileStatus( 114 projectRoot, 115 target, 116 uri, 117 ); 118 119 const decoration = DECORATION[status]; 120 newDecorations.set(uri.toString(), decoration); 121 } 122 123 return newDecorations; 124 } 125 126 /** Update file decorations per the current target's active files. */ 127 update = async (target?: string) => { 128 logger.info('Updating inactive file indicators'); 129 const newDecorations = await this.refreshDecorations(target); 130 131 const updatedUris = new Set([ 132 ...this.decorations.keys(), 133 ...newDecorations.keys(), 134 ]); 135 136 // This alone may not have any effect until VSC decides it needs to request 137 // new decorations for a file. 138 this.decorations = newDecorations; 139 140 // Firing this event notifies VSC that all of these files' decorations have 141 // changed. 142 didChangeFileDecorations.fire( 143 [...updatedUris.values()].map((it) => Uri.parse(it, true)), 144 ); 145 }; 146} 147