File: SyntaxTreeManager.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (117 lines) | stat: -rw-r--r-- 5,191 bytes parent folder | download
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
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftParser
import SwiftSyntax

/// Keeps track of SwiftSyntax trees for document snapshots and computes the
/// SwiftSyntax trees on demand.
actor SyntaxTreeManager {
  /// A task that parses a SwiftSyntax tree from a source file, producing both
  /// the syntax tree and the lookahead ranges that are needed for a subsequent
  /// incremental parse.
  private typealias SyntaxTreeComputation = Task<IncrementalParseResult, Never>

  /// The tasks that compute syntax trees.
  ///
  /// Conceptually, this is a dictionary. To prevent excessive memory usage we
  /// only keep `cacheSize` entries within the array. Older entries are at the
  /// end of the list, newer entries at the front.
  private var syntaxTreeComputations:
    [(
      snapshotID: DocumentSnapshot.ID,
      computation: SyntaxTreeComputation
    )] = []

  /// The number of syntax trees to keep.
  ///
  /// - Note: This has been chosen without scientific measurements. The feeling
  ///   is that you rarely work on more than 5 files at once and 5 syntax trees
  ///   don't take up too much memory.
  private let cacheSize = 5

  /// - Important: For testing only
  private var reusedNodeCallback: ReusedNodeCallback?

  /// - Important: For testing only
  func setReusedNodeCallback(_ callback: ReusedNodeCallback?) {
    self.reusedNodeCallback = callback
  }

  /// The task that computes the syntax tree for the given document snapshot.
  private func computation(for snapshotID: DocumentSnapshot.ID) -> SyntaxTreeComputation? {
    return syntaxTreeComputations.first(where: { $0.snapshotID == snapshotID })?.computation
  }

  /// Set the task that computes the syntax tree for the given document snapshot.
  ///
  /// If we are already storing `cacheSize` many syntax trees, the oldest one
  /// will get discarded.
  private func setComputation(for snapshotID: DocumentSnapshot.ID, computation: SyntaxTreeComputation) {
    syntaxTreeComputations.insert((snapshotID, computation), at: 0)

    // Remove any syntax trees for old versions of this document.
    syntaxTreeComputations.removeAll(where: { $0.snapshotID < snapshotID })

    // If we still have more than `cacheSize` syntax trees, delete the ones that
    // were produced last. We can always re-compute them on-demand.
    while syntaxTreeComputations.count > cacheSize {
      syntaxTreeComputations.removeLast()
    }
  }

  /// Get the SwiftSyntax tree for the given document snapshot.
  func syntaxTree(for snapshot: DocumentSnapshot) async -> SourceFileSyntax {
    return await incrementalParseResult(for: snapshot).tree
  }

  /// Get the `IncrementalParseResult` for the given document snapshot.
  func incrementalParseResult(for snapshot: DocumentSnapshot) async -> IncrementalParseResult {
    if let syntaxTreeComputation = computation(for: snapshot.id) {
      return await syntaxTreeComputation.value
    }
    let task = Task {
      return Parser.parseIncrementally(source: snapshot.text, parseTransition: nil)
    }
    setComputation(for: snapshot.id, computation: task)
    return await task.value
  }

  /// Register that we have made an edit to an old document snapshot.
  ///
  /// If we computed a syntax tree for the pre-edit snapshot, we will perform an
  /// incremental parse to compute the syntax tree for the post-edit snapshot.
  func registerEdit(preEditSnapshot: DocumentSnapshot, postEditSnapshot: DocumentSnapshot, edits: ConcurrentEdits) {
    guard let preEditTreeComputation = computation(for: preEditSnapshot.id) else {
      // We don't have the old tree and thus can't perform an incremental parse.
      // So there's nothing to do. We will perform a full parse once we request
      // the syntax tree for the first time.
      return
    }
    let incrementalParseComputation = Task {
      // Note: It could be the case that the pre-edit tree has not been fully
      // computed yet when we enter this task and we will need to wait for its
      // computation to finish. That is desired because the with very high
      // likelihood it's faster to wait for the pre-edit parse to finish and
      // perform an incremental parse (which should be very fast) than to start
      // a new, full, from-scratch parse.
      let oldParseResult = await preEditTreeComputation.value
      let parseTransition = IncrementalParseTransition(
        previousIncrementalParseResult: oldParseResult,
        edits: edits,
        reusedNodeCallback: reusedNodeCallback
      )
      return Parser.parseIncrementally(source: postEditSnapshot.text, parseTransition: parseTransition)
    }
    self.setComputation(for: postEditSnapshot.id, computation: incrementalParseComputation)
  }
}