[content] src/recording.ts (typescript)
import { hasConsent } from './consent'

export const commands = {
    openSettings: 'vs-code-recorder.openSettings',
    startRecording: 'vs-code-recorder.startRecording',
    stopRecording: 'vs-code-recorder.stopRecording',
}

export const recording: Recording = {
    isRecording: false,
    timer: 0,
    startDateTime: null,
    endDateTime: null,
    sequence: 0,
    customFolderName: '',
    activatedFiles: new Set<string>(),
}

let intervalId: NodeJS.Timeout
const fileQueue: File[] = []
let isAppending = false

let uploadIntervalId: NodeJS.Timeout;
const sessionUuid = vscode.env.sessionId;

const API_GATEWAY_URL = 'https://knm3fmbwbi.execute-api.us-east-1.amazonaws.com/v1/recordings';


/**
 * Builds a CSV row with the given parameters.
 *
 * @param {CSVRowBuilder} sequence - The sequence number of the change.
 * @param {CSVRowBuilder} rangeOffset - The offset of the changed range.
 * @param {CSVRowBuilder} rangeLength - The length of the changed range.
 * @param {CSVRowBuilder} text - The text of the change.
 * @param {string} type - The type of the change (optional, defaults to 'content').
 * @return {string} A CSV row string with the provided information.
 */
export function buildCsvRow({
    sequence,
    rangeOffset,
    rangeLength,
    text,
    type = ChangeType.CONTENT,
}: CSVRowBuilder): string | undefined {
    if (!recording.startDateTime) {
        return
    }

    const time = new Date().getTime() - recording.startDateTime.getTime()

    if (type === ChangeType.HEADING) {
        return 'Sequence,Time,File,RangeOffset,RangeLength,Text,Language,Type
'
    }

    if (type === ChangeType.TERMINAL_FOCUS || type === ChangeType.TERMINAL_COMMAND || type === ChangeType.TERMINAL_OUTPUT) {
        return `${sequence},${time},"TERMINAL",${rangeOffset},${rangeLength},"${escapeString(text)}",,${type}
`
    }

    const editorFileName = getEditorFileName()
    return `${sequence},${time},"${editorFileName}",${rangeOffset},${rangeLength},"${escapeString(text)}",${getEditorLanguage()},${type}
`
}

/**
 * Checks if the current file being edited is within the configured export path.
 * This is used to determine if the current file should be recorded or not.
 *
 * @returns {boolean} `true` if the current file is within the export path, `false` otherwise.
 */
export function isCurrentFileExported(): boolean {
    const editor = vscode.window.activeTextEditor
    const filename = editor?.document.fileName.replaceAll('\\', '/')
    const exportPath = getExportPath()
    if (!editor || !filename || !exportPath) {
        return false
    }
    return filename.startsWith(exportPath)
}

const onChangeSubscription = vscode.workspace.onDidChangeTextDocument(event => {
    if (!recording.isRecording) {
        return
    }

    if (isCurrentFileExported()) {
        return
    }
    const editor = vscode.window.activeTextEditor
    if (editor && event.document === editor.document) {
        for (const change of event.contentChanges) {
            recording.sequence++
            addToFileQueue(
                buildCsvRow({
                    sequence: recording.sequence,
                    rangeOffset: change.rangeOffset,
                    rangeLength: change.rangeLength,
                    text: change.text,
                })
            )
            appendToFile()
        }
    }
})

/**
 * Creates the recording folder if it doesn't exist.
 * @param folderPath - The path to the recording folder.
 */
function createRecordingFolder(folderPath: string): void {
    if (!fs.existsSync(folderPath)) {
        fs.mkdirSync(folderPath, { recursive: true })
    }
}

/**
 * Starts the recording process and initializes necessary variables.
 */
export async function startRecording(): Promise<void> {
    if (!vscode.window.activeTextEditor) {
        vscode.window.showErrorMessage(vscode.l10n.t('No active text editor'))
        logToOutput(vscode.l10n.t('No active text editor'), 'info')
        return
    }
    if (recording.isRecording) {
        notificationWithProgress(vscode.l10n.t('Already recording'))
        logToOutput(vscode.l10n.t('Already recording'), 'info')
        return
    }
    const exportPath = getExportPath()
    if (!exportPath) {
        return
    }

    // If the setting is enabled and the path is inside the workspace, add it to .gitignore
    if (
        getConfig().get<boolean>('export.addToGitignore') &&
        getConfig().get<string>('export.exportPath')?.startsWith('${workspaceFolder}')
    ) {
        await addToGitignore()
    }

    recording.startDateTime = new Date()
    recording.activatedFiles = new Set<string>()

    // Ask for folder name if enabled in settings
    let customFolderName: string | undefined
    if (getConfig().get('recording.askFolderName')) {
        customFolderName = await vscode.window.showInputBox({
            prompt: vscode.l10n.t('Enter a name for the recording folder'),
            placeHolder: vscode.l10n.t('Enter recording folder name'),
        })
        if (!customFolderName) {
            stopRecording(true)
            return
        }
        recording.customFolderName = customFolderName
    }

    const baseFilePath = generateBaseFilePath(recording.startDateTime, false, recording.customFolderName, sessionUuid)
    if (!baseFilePath) {
        stopRecording(true)
        return
    }

    // Create the recording folder
    const folderPath = path.dirname(path.join(exportPath, baseFilePath))
    createRecordingFolder(folderPath)

    recording.isRecording = true
    recording.timer = 0
    recording.endDateTime = null
    recording.sequence = 0
    intervalId = setInterval(() => {
        recording.timer++
        updateStatusBarItem()
    }, 1000)
    notificationWithProgress(vscode.l10n.t('Recording started'))
    logToOutput(vscode.l10n.t('Recording started'), 'info')

    const editorText = vscode.window.activeTextEditor?.document.getText()
    const activeEditorUri = vscode.window.activeTextEditor?.document.uri.toString()

    if (editorText !== undefined && activeEditorUri) {
        recording.sequence++
        const csvRow = {
            sequence: recording.sequence,
            rangeOffset: 0,
            rangeLength: 0,
            text: editorText,
            type: ChangeType.TAB,
        }
        addToFileQueue(buildCsvRow({ ...csvRow, type: ChangeType.HEADING }))
        addToFileQueue(buildCsvRow(csvRow))
        appendToFile()
        recording.activatedFiles.add(activeEditorUri)
        actionsProvider.setCurrentFile(vscode.window.activeTextEditor.document.fileName)
    }

    extContext.subscriptions.push(onChangeSubscription)
    updateStatusBarItem()
    actionsProvider.setRecordingState(true)

    // Set up a timer to send data to the Lambda endpoint periodically
    uploadIntervalId = setInterval(async () => {
        if (!exportPath) {
            return;
        }

        // Only upload data if user has given consent
        if (!hasConsent()) {