<commit_before>import { Component, Injectable, OnInit, ViewChild } from '@angular/core'
import { Observable } from 'rxjs'

import * as CodeMirror from 'codemirror'
// FIXME: Find a proper way to import the mode's files
require('codemirror/mode/gfm/gfm')
require('codemirror/mode/javascript/javascript')

import { TextDelete, TextInsert }  from 'mute-structs'

import { DocService } from '../doc/doc.service'
import { CursorService } from './cursor.service'
import { EditorService } from './editor.service'

@Component({
  selector: 'mute-editor',
  templateUrl: './editor.component.html',
  styleUrls: [
    // FIXME: Importing CodeMirror's CSS here doesn't work.
    // Should find a proper way to do it.
    './editor.component.css'
  ],
  providers: [CursorService]
})

@Injectable()
export class EditorComponent implements OnInit {

  private editor: CodeMirror.Editor
  private docService: DocService
  private cursorService: CursorService
  private editorService: EditorService

  @ViewChild('editorElt') editorElt

  constructor(docService: DocService, cursorService: CursorService, editorService: EditorService) {
    this.docService = docService
    this.cursorService = cursorService
    this.editorService = editorService
  }

  ngOnInit() {
    this.editor = CodeMirror.fromTextArea(this.editorElt.nativeElement, {
      lineNumbers: false,
      lineWrapping: true,
      mode: {name: 'gfm', globalVars: true}
    })

    this.cursorService.init(this.editor)

    const operationStream: Observable<ChangeEvent> = Observable.fromEventPattern(
      (h: ChangeEventHandler) => {
        this.editor.on('change', h)
      },
      (h: ChangeEventHandler) => {
        this.editor.off('change', h)
      },
      (instance: CodeMirror.Editor, change: CodeMirror.EditorChange) => {
        return new ChangeEvent(instance, change)
      })
      .filter((changeEvent: ChangeEvent) => {
        // The change's origin indicates the kind of changes performed
        // When the application updates the editor programatically, this field remains undefined
        // Allow to filter the changes performed by our application
        return changeEvent.change.origin !== undefined && changeEvent.change.origin !== 'setValue'
      })

    const multipleOperationsStream: Observable<ChangeEvent[]> = operationStream
      .map((changeEvent: ChangeEvent) => {
        return [changeEvent]
      })
      /*
      .bufferTime(1000)
      .filter((changeEvents: ChangeEvent[]) => {
        // From time to time, the buffer returns an empty array
        // Allow to filter these cases
        return changeEvents.length > 0
      })
      */

    const textOperationsStream: Observable<(TextDelete | TextInsert)[][]> = multipleOperationsStream.map( (changeEvents: ChangeEvent[]) => {
      return changeEvents.map( (changeEvent: ChangeEvent ) => {
        return changeEvent.toTextOperations()
      })
    })

    textOperationsStream.subscribe((textOperations: (TextDelete | TextInsert)[][]) => {
      this.editorService.emitLocalTextOperations(textOperations)
    })

    this.docService.onDocValue.subscribe( (str: string) => {
      this.editor.setValue(str)
    })

    // multipleOperationsStream.subscribe(
    //   (changeEvents: ChangeEvent[]) => {
    //     console.log(`${changeEvents.length} operations:`)
    //     changeEvents.forEach((changeEvent: ChangeEvent) => {
    //       console.log(changeEvent.instance)
    //       console.log(changeEvent.change)
    //     })
    //   })

    this.docService.onRemoteOperations.subscribe( (textOperations: any[]) => {
      const doc: CodeMirror.Doc = this.editor.getDoc()

      log.info('operation:editor', 'applied: ', textOperations)

      textOperations.forEach( (textOperation: any) => {
        const from: CodeMirror.Position = doc.posFromIndex(textOperation.offset)
        if (textOperation instanceof TextInsert) {
          doc.replaceRange(textOperation.content, from)
        } else if (textOperation instanceof TextDelete) {
          const to: CodeMirror.Position = doc.posFromIndex(textOperation.offset + textOperation.length)
          doc.replaceRange('', from, to)
        }
      })
    })
  }
}

type ChangeEventHandler = (instance: CodeMirror.Editor, change: CodeMirror.EditorChange) => void

class ChangeEvent {
  instance: CodeMirror.Editor
  change: CodeMirror.EditorChange

  constructor(instance: CodeMirror.Editor, change: CodeMirror.EditorChange) {<commit_msg>fix(editor): Unsubscribe from streams when the component is destroyed<commit_after>import { Component, Injectable, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { Observable, Subscription } from 'rxjs'

import * as CodeMirror from 'codemirror'
// FIXME: Find a proper way to import the mode's files
require('codemirror/mode/gfm/gfm')
require('codemirror/mode/javascript/javascript')

import { TextDelete, TextInsert }  from 'mute-structs'

import { DocService } from '../doc/doc.service'
import { CursorService } from './cursor.service'
import { EditorService } from './editor.service'

@Component({
  selector: 'mute-editor',
  templateUrl: './editor.component.html',
  styleUrls: [
    // FIXME: Importing CodeMirror's CSS here doesn't work.
    // Should find a proper way to do it.
    './editor.component.css'
  ],
  providers: [CursorService]
})

@Injectable()
export class EditorComponent implements OnDestroy, OnInit {

  private editor: CodeMirror.Editor
  private docService: DocService
  private cursorService: CursorService
  private editorService: EditorService

  private docValueSubscription: Subscription
  private remoteOperationsSubscription: Subscription
  private localOperationsSubscription: Subscription

  @ViewChild('editorElt') editorElt

  constructor(docService: DocService, cursorService: CursorService, editorService: EditorService) {
    this.docService = docService
    this.cursorService = cursorService
    this.editorService = editorService
  }

  ngOnInit() {
    this.editor = CodeMirror.fromTextArea(this.editorElt.nativeElement, {
      lineNumbers: false,
      lineWrapping: true,
      mode: {name: 'gfm', globalVars: true}
    })

    this.cursorService.init(this.editor)

    const operationStream: Observable<ChangeEvent> = Observable.fromEventPattern(
      (h: ChangeEventHandler) => {
        this.editor.on('change', h)
      },
      (h: ChangeEventHandler) => {
        this.editor.off('change', h)
      },
      (instance: CodeMirror.Editor, change: CodeMirror.EditorChange) => {
        return new ChangeEvent(instance, change)
      })
      .filter((changeEvent: ChangeEvent) => {
        // The change's origin indicates the kind of changes performed
        // When the application updates the editor programatically, this field remains undefined
        // Allow to filter the changes performed by our application
        return changeEvent.change.origin !== undefined && changeEvent.change.origin !== 'setValue'
      })

    const multipleOperationsStream: Observable<ChangeEvent[]> = operationStream
      .map((changeEvent: ChangeEvent) => {
        return [changeEvent]
      })
      /*
      .bufferTime(1000)
      .filter((changeEvents: ChangeEvent[]) => {
        // From time to time, the buffer returns an empty array
        // Allow to filter these cases
        return changeEvents.length > 0
      })
      */

    const textOperationsStream: Observable<(TextDelete | TextInsert)[][]> = multipleOperationsStream.map( (changeEvents: ChangeEvent[]) => {
      return changeEvents.map( (changeEvent: ChangeEvent ) => {
        return changeEvent.toTextOperations()
      })
    })

    this.localOperationsSubscription = textOperationsStream.subscribe((textOperations: (TextDelete | TextInsert)[][]) => {
      this.editorService.emitLocalTextOperations(textOperations)
    })

    this.docValueSubscription = this.docService.onDocValue.subscribe( (str: string) => {
      this.editor.setValue(str)
    })

    // multipleOperationsStream.subscribe(
    //   (changeEvents: ChangeEvent[]) => {
    //     console.log(`${changeEvents.length} operations:`)
    //     changeEvents.forEach((changeEvent: ChangeEvent) => {
    //       console.log(changeEvent.instance)
    //       console.log(changeEvent.change)
    //     })
    //   })

    this.remoteOperationsSubscription = this.docService.onRemoteOperations.subscribe( (textOperations: any[]) => {
      const doc: CodeMirror.Doc = this.editor.getDoc()

      log.info('operation:editor', 'applied: ', textOperations)

      textOperations.forEach( (textOperation: any) => {
        const from: CodeMirror.Position = doc.posFromIndex(textOperation.offset)
        if (textOperation instanceof TextInsert) {
          doc.replaceRange(textOperation.content, from)
        } else if (textOperation instanceof TextDelete) {
          const to: CodeMirror.Position = doc.posFromIndex(textOperation.offset + textOperation.length)
          doc.replaceRange('', from, to)
        }
      })
    })
  }

  ngOnDestroy () {
    this.docValueSubscription.unsubscribe()
    this.localOperationsSubscription.unsubscribe()
    this.remoteOperationsSubscription.unsubscribe()
  }

}

type ChangeEventHandler = (instance: CodeMirror.Editor, change: CodeMirror.EditorChange) => void

class ChangeEvent {
  instance: CodeMirror.Editor
  change: CodeMirror.EditorChange

  constructor(instance: CodeMirror.Editor, change: CodeMirror.EditorChange) {
{'fraction_non_alphanumeric': 0.11447061319753872, 'fraction_numerical': 0.0010608953957139827, 'mean_word_length': 3.417525773195876, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 41, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5089356', 'n_tokens_mistral': 2581, 'n_tokens_neox': 2561, 'n_words': 591}