about summary refs log blame commit diff
path: root/users/Profpatsch/lyric/extension/src/extension.ts
blob: ded2a8ab0d9cf1282cc23f3c960b30e6ab5692a3 (plain) (tree)
1
2
3
4
5
6
7
8
9





                                                                     


                                                                                      
 

                                                
 



                                                                    
 
                                       

                                                 
 



                                        
 



                                               
 
                                  
 












                                                                             










                                                                                                                                             
                                                                                          





                                                           
                                                                                          




                                                         
                                                                  



                                                            
                                                                  





                        
                                


                                           
                                
 


                                 




























































                                                                                          
                                                      















                                                                                  
                                                              
































































                                                                                          
import * as vscode from 'vscode';
import * as net from 'net';

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(...registerCheckLineTimestamp(context));
  context.subscriptions.push(
    vscode.commands.registerCommand('extension.jumpToLrcPosition', jumpToLrcPosition),
  );
}

function jumpToLrcPosition() {
  const editor = vscode.window.activeTextEditor;

  if (!editor) {
    vscode.window.showInformationMessage('No active editor found.');
    return;
  }

  const ext = new Ext(editor.document);
  const position = editor.selection.active;
  const res = ext.getTimestampFromLine(position);

  if (!res) {
    return;
  }
  const { milliseconds, seconds } = res;

  // Prepare JSON command to send to the socket
  const jsonCommand = {
    command: ['seek', seconds, 'absolute'],
  };

  const socket = new net.Socket();

  const socketPath = process.env.HOME + '/tmp/mpv-socket';
  socket.connect(socketPath, () => {
    socket.write(JSON.stringify(jsonCommand));
    socket.write('\n');
    vscode.window.showInformationMessage(
      `Sent command to jump to [${formatTimestamp(milliseconds)}].`,
    );
    socket.end();
  });

  socket.on('error', err => {
    vscode.window.showErrorMessage(`Failed to send command: ${err.message}`);
  });
}

// If the difference to the timestamp on the next line is larger than 10 seconds, underline the next line and show a warning message on hover
export function registerCheckLineTimestamp(_context: vscode.ExtensionContext) {
  const changesToCheck: Set<vscode.TextDocument> = new Set();
  const everSeen = new Set<vscode.TextDocument>();

  return [
    vscode.workspace.onDidChangeTextDocument(e => {
      changesToCheck.add(e.document);
      if (vscode.window.activeTextEditor?.document === e.document) {
        doEditorChecks(vscode.window.activeTextEditor.document, everSeen, changesToCheck);
      }
    }),
    vscode.workspace.onDidOpenTextDocument(e => {
      changesToCheck.add(e);
      everSeen.add(e);
      if (vscode.window.activeTextEditor?.document === e) {
        doEditorChecks(vscode.window.activeTextEditor.document, everSeen, changesToCheck);
      }
    }),

    vscode.window.onDidChangeActiveTextEditor(editor => {
      if (editor) {
        doEditorChecks(editor.document, everSeen, changesToCheck);
      }
    }),
    vscode.window.onDidChangeVisibleTextEditors(editors => {
      for (const editor of editors) {
        doEditorChecks(editor.document, everSeen, changesToCheck);
      }
    }),
  ];
}

function doEditorChecks(
  document: vscode.TextDocument,
  everSeen: Set<vscode.TextDocument>,
  changesToCheck: Set<vscode.TextDocument>,
) {
  const ext = new Ext(document);

  if (!everSeen.has(document)) {
    changesToCheck.add(document);
    everSeen.add(document);
  }

  if (!changesToCheck.has(document)) {
    return;
  }
  changesToCheck.delete(document);

  const from = 0;
  const to = document.lineCount - 1;
  for (let line = from; line <= to; line++) {
    const warnings: string[] = [];
    const timeDiff = timeDifferenceTooLarge(ext, line);
    if (timeDiff !== undefined) {
      warnings.push(timeDiff);
    }
    const nextTimestampSmaller = nextLineTimestampSmallerThanCurrent(ext, line);
    if (nextTimestampSmaller !== undefined) {
      warnings.push(nextTimestampSmaller);
    }
    for (const warning of warnings) {
      global_manageWarnings.setWarning(document, line, warning);
    }
    // unset any warnings if this doesn’t apply anymore
    if (warnings.length === 0) {
      global_manageWarnings.setWarning(document, line);
    }
  }
}

/** Warn if the difference to the timestamp on the next line is larger than 10 seconds */
function timeDifferenceTooLarge(ext: Ext, line: number): string | undefined {
  const timeDifference = ext.getTimeDifferenceToNextLineTimestamp(
    new vscode.Position(line, 0),
  );
  if (
    !timeDifference ||
    timeDifference.thisLineIsEmpty ||
    timeDifference.difference <= 10000
  ) {
    return;
  }
  return `Time difference to next line is ${formatTimestamp(timeDifference.difference)}`;
}

/** Warn if the timestamp on the next line is smaller or equal to the current timestamp */
function nextLineTimestampSmallerThanCurrent(ext: Ext, line: number): string | undefined {
  const timeDifference = ext.getTimeDifferenceToNextLineTimestamp(
    new vscode.Position(line, 0),
  );
  if (!timeDifference) {
    return;
  }
  if (timeDifference.difference == 0) {
    return `The timestamp to the next line is not increasing`;
  }
  if (timeDifference.difference < 0) {
    return `The timestamp to the next line is decreasing`;
  }
}

class Ext {
  constructor(public document: vscode.TextDocument) {}

  getTimeDifferenceToNextLineTimestamp(position: vscode.Position) {
    const thisLineTimestamp = this.getTimestampFromLine(position);
    const nextLineTimestamp = this.getTimestampFromLine(
      position.with({ line: position.line + 1 }),
    );
    if (!thisLineTimestamp || !nextLineTimestamp) {
      return;
    }
    return {
      difference: nextLineTimestamp.milliseconds - thisLineTimestamp.milliseconds,
      thisLineIsEmpty: thisLineTimestamp.text.trim() === '',
    };
  }

  getTimestampFromLine(position: vscode.Position) {
    const lineText = this.document.lineAt(position.line).text;

    // Extract timestamp [mm:ss.ms] from the current line
    const match = lineText.match(/\[(\d+:\d+\.\d+)\](.*)/);
    if (!match) {
      return;
    }
    const [, timestamp, text] = match!;
    const milliseconds = parseTimestamp(timestamp);
    const seconds = milliseconds / 1000;
    return { milliseconds, seconds, text };
  }
}

function parseTimestamp(timestamp: string): number {
  // Parse [mm:ss.ms] format into milliseconds
  const [min, sec] = timestamp.split(':');

  const minutes = parseInt(min, 10);
  const seconds = parseFloat(sec);

  return minutes * 60 * 1000 + seconds * 1000;
}

function formatTimestamp(ms: number): string {
  // Format milliseconds back into [mm:ss.ms]
  const minutes = Math.floor(ms / 60000);
  ms %= 60000;
  const seconds = (ms / 1000).toFixed(2);

  return `${String(minutes).padStart(2, '0')}:${seconds}`;
}

class ManageWarnings {
  private warnings: Map<number, string> = new Map();
  private diagnostics: vscode.DiagnosticCollection;

  constructor() {
    this.diagnostics = vscode.languages.createDiagnosticCollection();
  }

  /** Set a warning message on a line in a document, if null then unset */
  setWarning(document: vscode.TextDocument, line: number, message?: string) {
    if (message !== undefined) {
      this.warnings.set(line, message);
    } else {
      this.warnings.delete(line);
    }
    this.updateDiagnostics(document);
  }

  private updateDiagnostics(document: vscode.TextDocument) {
    const mkWarning = (line: number, message: string) => {
      const lineRange = document.lineAt(line).range;
      return new vscode.Diagnostic(lineRange, message, vscode.DiagnosticSeverity.Warning);
    };
    const diagnostics: vscode.Diagnostic[] = [];
    for (const [line, message] of this.warnings) {
      diagnostics.push(mkWarning(line, message));
    }
    this.diagnostics.delete(document.uri);
    this.diagnostics.set(document.uri, diagnostics);
  }
}

const global_manageWarnings = new ManageWarnings();