xref: /aosp_15_r20/external/pigweed/pw_ide/ts/pigweed-vscode/src/inactiveFileDecoration.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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