Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 748efa1

Browse files
committed
Handle document changes in implicits
1 parent 7e87fb7 commit 748efa1

File tree

5 files changed

+269
-133
lines changed

5 files changed

+269
-133
lines changed

lib/syntax_tree/language_server.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def implicits(source)
8383
before: implicits.before.map(&serialize),
8484
after: implicits.after.map(&serialize)
8585
}
86+
rescue ParseError
8687
end
8788

8889
def write(value)

vscode/src/Implicits.ts

Lines changed: 145 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
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";
22
import { LanguageClient } from "vscode-languageclient/node";
33

44
type Implicit = { position: number, text: string };
5-
type ImplicitsResponse = { before: Implicit[], after: Implicit[] };
5+
type ImplicitSet = { before: Implicit[], after: Implicit[] };
66

77
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[];
1121

1222
constructor(languageClient: LanguageClient, outputChannel: OutputChannel) {
1323
this.languageClient = languageClient;
@@ -20,25 +30,95 @@ class Implicits implements Disposable {
2030
rangeBehavior: DecorationRangeBehavior.ClosedClosed
2131
});
2232

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);
2442
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+
];
2654
}
2755

2856
dispose() {
29-
this.decorationType.dispose();
57+
this.disposables.forEach((disposable) => disposable.dispose());
3058
}
3159

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);
3567
}
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);
3678

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.
3799
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() }
40102
});
41103

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+
42122
const decorations: DecorationOptions[] = [
43123
...implicits.before.map(({ position, text: contentText }) => ({
44124
range: new Range(editor.document.positionAt(position), editor.document.positionAt(position)),
@@ -53,6 +133,58 @@ class Implicits implements Disposable {
53133
this.outputChannel.appendLine("Settings implicits");
54134
editor.setDecorations(this.decorationType, decorations);
55135
}
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;
56188
}
57189

58190
export default Implicits;

vscode/src/Visualize.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { commands, Disposable, languages, OutputChannel, ProviderResult, TextDocumentContentProvider, Uri, ViewColumn, window, workspace } from "vscode";
2+
import { LanguageClient } from "vscode-languageclient/node";
3+
4+
class Visualize implements Disposable, TextDocumentContentProvider {
5+
// The client used to communicate with the language server.
6+
private readonly languageClient: LanguageClient;
7+
8+
// The output channel used for logging for this class. It's given from the
9+
// main file so that it uses the same as the rest of the extension.
10+
private readonly outputChannel: OutputChannel;
11+
12+
// The list of callbacks and objects that should be disposed when an instance
13+
// of Visualize is being disposed.
14+
private readonly disposables: Disposable[];
15+
16+
constructor(languageClient: LanguageClient, outputChannel: OutputChannel) {
17+
this.languageClient = languageClient;
18+
this.outputChannel = outputChannel;
19+
this.disposables = [
20+
commands.registerCommand("syntaxTree.visualize", this.visualize),
21+
workspace.registerTextDocumentContentProvider("syntaxTree", this)
22+
];
23+
}
24+
25+
dispose() {
26+
this.disposables.forEach((disposable) => disposable.dispose());
27+
}
28+
29+
provideTextDocumentContent(uri: Uri): ProviderResult<string> {
30+
this.outputChannel.appendLine("Requesting visualization");
31+
return this.languageClient.sendRequest("syntaxTree/visualizing", { textDocument: { uri: uri.path } });
32+
}
33+
34+
async visualize() {
35+
const document = window.activeTextEditor?.document;
36+
37+
if (document && document.languageId === "ruby" && document.uri.scheme === "file") {
38+
const uri = Uri.parse(`syntaxTree:${document.uri.toString()}`);
39+
40+
const doc = await workspace.openTextDocument(uri);
41+
languages.setTextDocumentLanguage(doc, "plaintext");
42+
43+
await window.showTextDocument(doc, ViewColumn.Beside, true);
44+
}
45+
}
46+
}
47+
48+
export default Visualize;

0 commit comments

Comments
 (0)