summaryrefslogtreecommitdiff
path: root/packages/vscode-ide-companion/src/recent-files-manager.ts
blob: 317cc903f065f8851607bafa565994828659fb72 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import * as vscode from 'vscode';

export const MAX_FILES = 10;
export const MAX_FILE_AGE_MINUTES = 5;

interface RecentFile {
  uri: vscode.Uri;
  timestamp: number;
}

/**
 * Keeps track of the 10 most recently-opened files
 * opened less than 5 min ago. If a file is closed or deleted,
 * it will be removed. If the max length is reached, older files will get removed first.
 */
export class RecentFilesManager {
  private readonly files: RecentFile[] = [];
  private readonly onDidChangeEmitter = new vscode.EventEmitter<void>();
  readonly onDidChange = this.onDidChangeEmitter.event;
  private debounceTimer: NodeJS.Timeout | undefined;

  constructor(private readonly context: vscode.ExtensionContext) {
    const editorWatcher = vscode.window.onDidChangeActiveTextEditor(
      (editor) => {
        if (editor) {
          this.add(editor.document.uri);
        }
      },
    );
    const deleteWatcher = vscode.workspace.onDidDeleteFiles((event) => {
      for (const uri of event.files) {
        this.remove(uri);
      }
    });
    const closeWatcher = vscode.workspace.onDidCloseTextDocument((document) => {
      this.remove(document.uri);
    });
    const renameWatcher = vscode.workspace.onDidRenameFiles((event) => {
      for (const { oldUri, newUri } of event.files) {
        this.remove(oldUri, false);
        this.add(newUri);
      }
    });

    const selectionWatcher = vscode.window.onDidChangeTextEditorSelection(
      () => {
        this.fireWithDebounce();
      },
    );

    context.subscriptions.push(
      editorWatcher,
      deleteWatcher,
      closeWatcher,
      renameWatcher,
      selectionWatcher,
    );
  }

  private fireWithDebounce() {
    if (this.debounceTimer) {
      clearTimeout(this.debounceTimer);
    }
    this.debounceTimer = setTimeout(() => {
      this.onDidChangeEmitter.fire();
    }, 50); // 50ms
  }

  private remove(uri: vscode.Uri, fireEvent = true) {
    const index = this.files.findIndex(
      (file) => file.uri.fsPath === uri.fsPath,
    );
    if (index !== -1) {
      this.files.splice(index, 1);
      if (fireEvent) {
        this.fireWithDebounce();
      }
    }
  }

  add(uri: vscode.Uri) {
    if (uri.scheme !== 'file') {
      return;
    }

    this.remove(uri, false);
    this.files.unshift({ uri, timestamp: Date.now() });

    if (this.files.length > MAX_FILES) {
      this.files.pop();
    }
    this.fireWithDebounce();
  }

  get recentFiles(): Array<{ filePath: string; timestamp: number }> {
    const now = Date.now();
    const maxAgeInMs = MAX_FILE_AGE_MINUTES * 60 * 1000;
    return this.files
      .filter((file) => now - file.timestamp < maxAgeInMs)
      .map((file) => ({
        filePath: file.uri.fsPath,
        timestamp: file.timestamp,
      }));
  }
}