aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/js/xtext/OccurrencesService.ts
blob: bc865537fc2a5a634ce85ce2ffa6d1e5b99fdd2a (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
117
118
119
120
121
122
123
124
125
126
127
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 {
  isConflictResult,
  occurrencesResult,
  TextRegion,
} 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: TextRegion[]): 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 || isConflictResult(result, '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;
    }
    const parsedOccurrencesResult = occurrencesResult.safeParse(result);
    if (!parsedOccurrencesResult.success) {
      log.error(
        'Unexpected occurences result',
        result,
        'not an OccurrencesResult: ',
        parsedOccurrencesResult.error,
      );
      this.clearOccurrences();
      return;
    }
    const { stateId, writeRegions, readRegions } = parsedOccurrencesResult.data;
    if (stateId !== this.updateService.xtextStateId) {
      log.error('Unexpected state id, expected:', this.updateService.xtextStateId, 'got:', stateId);
      this.clearOccurrences();
      return;
    }
    const write = transformOccurrences(writeRegions);
    const read = transformOccurrences(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;
    }
  }
}