aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/xtext/OccurrencesService.ts
blob: d1dec9e90b401eaf5244badf53eb0c04354783f1 (plain) (blame)
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
112
113
114
115
116
import { Transaction } from '@codemirror/state';

import type { EditorStore } from '../editor/EditorStore';
import type { IOccurrence } from '../editor/findOccurrences';
import type { UpdateService } from './UpdateService';
import { getLogger } from '../utils/logger';
import { Timer } from '../utils/Timer';
import { XtextWebSocketClient } from './XtextWebSocketClient';
import {
  isOccurrencesResult,
  isServiceConflictResult,
  ITextRegion,
} from './xtextServiceResults';

const FIND_OCCURRENCES_TIMEOUT_MS = 1000;

// Must clear occurrences asynchronously from `onTransaction`,
// because we must not emit a conflicting transaction when handling the pending transaction.
const CLEAR_OCCURRENCES_TIMEOUT_MS = 10;

const log = getLogger('xtext.OccurrencesService');

function transformOccurrences(regions: ITextRegion[]): IOccurrence[] {
  const occurrences: IOccurrence[] = [];
  regions.forEach(({ offset, length }) => {
    if (length > 0) {
      occurrences.push({
        from: offset,
        to: offset + length,
      });
    }
  });
  return occurrences;
}

export class OccurrencesService {
  private readonly store: EditorStore;

  private readonly webSocketClient: XtextWebSocketClient;

  private readonly updateService: UpdateService;

  private hasOccurrences = false;

  private readonly findOccurrencesTimer = new Timer(() => {
    this.handleFindOccurrences();
  }, FIND_OCCURRENCES_TIMEOUT_MS);

  private readonly clearOccurrencesTimer = new Timer(() => {
    this.clearOccurrences();
  }, CLEAR_OCCURRENCES_TIMEOUT_MS);

  constructor(
    store: EditorStore,
    webSocketClient: XtextWebSocketClient,
    updateService: UpdateService,
  ) {
    this.store = store;
    this.webSocketClient = webSocketClient;
    this.updateService = updateService;
  }

  onTransaction(transaction: Transaction): void {
    if (transaction.docChanged) {
      this.clearOccurrencesTimer.schedule();
      this.findOccurrencesTimer.reschedule();
    }
    if (transaction.isUserEvent('select')) {
      this.findOccurrencesTimer.reschedule();
    }
  }

  private handleFindOccurrences() {
    this.clearOccurrencesTimer.cancel();
    this.updateOccurrences().catch((error) => {
      log.error('Unexpected error while updating occurrences', error);
      this.clearOccurrences();
    });
  }

  private async updateOccurrences() {
    await this.updateService.update();
    const result = await this.webSocketClient.send({
      resource: this.updateService.resourceName,
      serviceType: 'occurrences',
      expectedStateId: this.updateService.xtextStateId,
      caretOffset: this.store.state.selection.main.head,
    });
    const allChanges = this.updateService.computeChangesSinceLastUpdate();
    if (!allChanges.empty
      || (isServiceConflictResult(result) && result.conflict === 'canceled')) {
      // Stale occurrences result, the user already made some changes.
      // We can safely ignore the occurrences and schedule a new find occurrences call.
      this.clearOccurrences();
      this.findOccurrencesTimer.schedule();
      return;
    }
    if (!isOccurrencesResult(result) || result.stateId !== this.updateService.xtextStateId) {
      log.error('Unexpected occurrences result', result);
      this.clearOccurrences();
      return;
    }
    const write = transformOccurrences(result.writeRegions);
    const read = transformOccurrences(result.readRegions);
    this.hasOccurrences = write.length > 0 || read.length > 0;
    log.debug('Found', write.length, 'write and', read.length, 'read occurrences');
    this.store.updateOccurrences(write, read);
  }

  private clearOccurrences() {
    if (this.hasOccurrences) {
      this.store.updateOccurrences([], []);
      this.hasOccurrences = false;
    }
  }
}