1
- import { DecorationOptions , DecorationRangeBehavior , Disposable , OutputChannel , Range , TextEditor , TextEditorDecorationType , ThemeColor , window } from "vscode" ;
1
+ import { DecorationOptions , DecorationRangeBehavior , Disposable , OutputChannel , Range , TextDocument , TextDocumentChangeEvent , TextEditor , TextEditorDecorationType , ThemeColor , window , workspace } from "vscode" ;
2
2
import { LanguageClient } from "vscode-languageclient/node" ;
3
3
4
4
type Implicit = { position : number , text : string } ;
5
- type ImplicitsResponse = { before : Implicit [ ] , after : Implicit [ ] } ;
5
+ type ImplicitSet = { before : Implicit [ ] , after : Implicit [ ] } ;
6
6
7
7
class Implicits implements Disposable {
8
- private languageClient : LanguageClient ;
9
- private outputChannel : OutputChannel ;
10
- private decorationType : TextEditorDecorationType ;
8
+ // The client used to communicate with the language server. In the case of
9
+ // this class it's used to send a syntaxTree/implicits request.
10
+ private readonly languageClient : LanguageClient ;
11
+
12
+ // The output channel used for logging for this class. It's given from the
13
+ // main file so that it uses the same as the rest of the extension.
14
+ private readonly outputChannel : OutputChannel ;
15
+
16
+ private readonly decorationType : TextEditorDecorationType ;
17
+ private readonly implicitsCache : WeakMap < TextDocument , ImplicitSet > ;
18
+ private readonly debouncedHandleTextDocumentChange : Debounced < TextEditor > ;
19
+
20
+ private readonly disposables : Disposable [ ] ;
11
21
12
22
constructor ( languageClient : LanguageClient , outputChannel : OutputChannel ) {
13
23
this . languageClient = languageClient ;
@@ -20,25 +30,95 @@ class Implicits implements Disposable {
20
30
rangeBehavior : DecorationRangeBehavior . ClosedClosed
21
31
} ) ;
22
32
23
- for ( const editor of window . visibleTextEditors ) {
33
+ this . implicitsCache = new WeakMap ( ) ;
34
+ this . setImplicitsForEditors ( window . visibleTextEditors ) ;
35
+
36
+ // Here we're going to debounce the handleTextDocumentChange callback so
37
+ // that we're not reacting too quickly to user inputs and making it flash
38
+ // alll around the editor.
39
+ this . debouncedHandleTextDocumentChange = debounce ( 300 , ( editor : TextEditor ) => {
40
+ this . outputChannel . appendLine ( "Handling text document changes (debounced)" ) ;
41
+ this . implicitsCache . delete ( editor . document ) ;
24
42
this . setImplicitsForEditor ( editor ) ;
25
- }
43
+ } ) ;
44
+
45
+ // Track all of the various callbacks and objects that implement Disposable
46
+ // so that when we need to dispose of the entire Implicits instance we can
47
+ // iterate through and dispose all of them.
48
+ this . disposables = [
49
+ this . decorationType ,
50
+ window . onDidChangeVisibleTextEditors ( this . setImplicitsForEditors , this ) ,
51
+ workspace . onDidChangeTextDocument ( this . handleTextDocumentChange , this ) ,
52
+ this . debouncedHandleTextDocumentChange
53
+ ] ;
26
54
}
27
55
28
56
dispose ( ) {
29
- this . decorationType . dispose ( ) ;
57
+ this . disposables . forEach ( ( disposable ) => disposable . dispose ( ) ) ;
30
58
}
31
59
32
- async setImplicitsForEditor ( editor : TextEditor ) {
33
- if ( editor . document . languageId != "ruby" ) {
34
- return ;
60
+ handleTextDocumentChange ( event : TextDocumentChangeEvent ) {
61
+ const editor = window . activeTextEditor ;
62
+
63
+ if ( editor !== undefined && event . document === editor . document ) {
64
+ this . debouncedHandleTextDocumentChange ( editor ) ;
65
+ } else {
66
+ this . implicitsCache . delete ( event . document ) ;
35
67
}
68
+ }
69
+
70
+ // Asynchronously get the implicits for a given text document, optionally
71
+ // using a cache if it has already been populated.
72
+ async getImplicitsForTextDocument ( document : TextDocument ) : Promise < ImplicitSet | undefined > {
73
+ if ( document . languageId !== "ruby" ) {
74
+ // This editor may have previously been a Ruby file, but that has now
75
+ // changed. So we should delete the implicits that may be there.
76
+ if ( this . implicitsCache . has ( document ) ) {
77
+ this . implicitsCache . delete ( document ) ;
36
78
79
+ // Return an empty set of implicits so that it gets properly cleared
80
+ // from the document.
81
+ return { before : [ ] , after : [ ] } ;
82
+ }
83
+
84
+ // Otherwise, we're going to return undefined so that we don't bother with
85
+ // this file, as it's not a Ruby file.
86
+ return undefined ;
87
+ }
88
+
89
+ // Check the cache first to see if we have already computed the implicits
90
+ // for this document. Return them if we have them.
91
+ let implicits = this . implicitsCache . get ( document ) ;
92
+ if ( implicits ) {
93
+ this . outputChannel . appendLine ( "Loading implicits from cache" ) ;
94
+ return implicits ;
95
+ }
96
+
97
+ // Otherwise, asynchronously request the implicits from the language server,
98
+ // cache the response, and return it.
37
99
this . outputChannel . appendLine ( "Requesting implicits" ) ;
38
- const implicits = await this . languageClient . sendRequest < ImplicitsResponse > ( "syntaxTree/implicits" , {
39
- textDocument : { uri : editor . document . uri . toString ( ) }
100
+ implicits = await this . languageClient . sendRequest < ImplicitSet > ( "syntaxTree/implicits" , {
101
+ textDocument : { uri : document . uri . toString ( ) }
40
102
} ) ;
41
103
104
+ // In case of a syntax error, this is not going to return anything. In that
105
+ // case, we don't want to set the cache to anything, but we also don't want
106
+ // to clear the previous implicits either. So we're just going to return
107
+ // undefined
108
+ if ( ! implicits ) {
109
+ return undefined ;
110
+ }
111
+
112
+ this . implicitsCache . set ( document , implicits ) ;
113
+ return implicits ;
114
+ }
115
+
116
+ async setImplicitsForEditor ( editor : TextEditor ) {
117
+ const implicits = await this . getImplicitsForTextDocument ( editor . document ) ;
118
+ if ( ! implicits ) {
119
+ return ;
120
+ }
121
+
42
122
const decorations : DecorationOptions [ ] = [
43
123
...implicits . before . map ( ( { position, text : contentText } ) => ( {
44
124
range : new Range ( editor . document . positionAt ( position ) , editor . document . positionAt ( position ) ) ,
@@ -53,6 +133,58 @@ class Implicits implements Disposable {
53
133
this . outputChannel . appendLine ( "Settings implicits" ) ;
54
134
editor . setDecorations ( this . decorationType , decorations ) ;
55
135
}
136
+
137
+ setImplicitsForEditors ( editors : readonly TextEditor [ ] ) {
138
+ editors . forEach ( ( editor ) => this . setImplicitsForEditor ( editor ) ) ;
139
+ }
140
+ }
141
+
142
+ // The return value of the debounce function below.
143
+ type Debounced < T extends object > = Disposable & ( ( argument : T ) => void ) ;
144
+
145
+ // This function will take a given callback and delay it by a given delay. If
146
+ // another call to the same function with the same argument comes in before the
147
+ // first one finishes, it will cancel the first invocation and start the delay
148
+ // again.
149
+ function debounce < T extends object > ( delay : number , callback : ( argument : T ) => void ) : Debounced < T > {
150
+ let allTimeouts = new WeakMap < T , NodeJS . Timeout > ( ) ;
151
+ const liveTimeouts = new Set < NodeJS . Timeout > ( ) ;
152
+
153
+ const debounced = ( argument : T ) => {
154
+ // First, clear out the timeout for the previous call to this debounced
155
+ // callback.
156
+ const previousTimeout = allTimeouts . get ( argument ) ;
157
+ if ( previousTimeout !== undefined ) {
158
+ clearTimeout ( previousTimeout ) ;
159
+ liveTimeouts . delete ( previousTimeout ) ;
160
+ }
161
+
162
+ // Next, create a new timeout for this call to the debounced callback.
163
+ // If it doesn't get cancelled by a subsequent call, it will be invoked with
164
+ // the given argument.
165
+ const timeout = setTimeout ( ( ) => {
166
+ allTimeouts . delete ( argument ) ;
167
+ liveTimeouts . delete ( timeout ) ;
168
+ callback ( argument ) ;
169
+ } , delay ) ;
170
+
171
+ // Finally, track the timeout that we just created in both a WeakMap
172
+ // that links the editor to the timeout and a set. We track both so that
173
+ // we can iterate through the timeouts when VSCode needs to dispose of
174
+ // this subscription.
175
+ allTimeouts . set ( argument , timeout ) ;
176
+ liveTimeouts . add ( timeout ) ;
177
+ } ;
178
+
179
+ // Define the necessary dispose function so that VSCode knows how to
180
+ // trigger disposal of this entire debounced function.
181
+ debounced . dispose = ( ) => {
182
+ liveTimeouts . forEach ( ( timeout ) => clearTimeout ( timeout ) ) ;
183
+ liveTimeouts . clear ( ) ;
184
+ allTimeouts = new WeakMap ( ) ;
185
+ } ;
186
+
187
+ return debounced ;
56
188
}
57
189
58
190
export default Implicits ;
0 commit comments