Skip to content

Commit ef0f5de

Browse files
Add event handler for editor selection in Documentation Live Preview editor (#1413)
* throttle convert requests to 10Hz * never steal focus when revealing the live preview editor * add selection even listener to detect cursor movement
1 parent 71f2932 commit ef0f5de

File tree

3 files changed

+136
-67
lines changed

3 files changed

+136
-67
lines changed

package-lock.json

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,6 +1566,7 @@
15661566
"@types/chai-subset": "^1.3.5",
15671567
"@types/glob": "^7.1.6",
15681568
"@types/lcov-parse": "^1.0.2",
1569+
"@types/lodash.throttle": "^4.1.9",
15691570
"@types/mocha": "^10.0.10",
15701571
"@types/mock-fs": "^4.13.4",
15711572
"@types/node": "^18.19.76",
@@ -1588,6 +1589,7 @@
15881589
"esbuild": "^0.25.0",
15891590
"eslint": "^8.57.0",
15901591
"eslint-config-prettier": "^10.0.1",
1592+
"lodash.throttle": "^4.1.1",
15911593
"mocha": "^10.8.2",
15921594
"mock-fs": "^5.5.0",
15931595
"node-pty": "^1.0.0",

src/documentation/DocumentationPreviewEditor.ts

Lines changed: 87 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { RenderNode, WebviewContent, WebviewMessage } from "./webview/WebviewMes
1919
import { WorkspaceContext } from "../WorkspaceContext";
2020
import { DocCDocumentationRequest, DocCDocumentationResponse } from "../sourcekit-lsp/extensions";
2121
import { LSPErrorCodes, ResponseError } from "vscode-languageclient";
22+
// eslint-disable-next-line @typescript-eslint/no-require-imports
23+
import throttle = require("lodash.throttle");
2224

2325
export enum PreviewEditorConstant {
2426
VIEW_TYPE = "swift.previewDocumentationEditor",
@@ -94,6 +96,7 @@ export class DocumentationPreviewEditor implements vscode.Disposable {
9496
}
9597

9698
private activeTextEditor?: vscode.TextEditor;
99+
private activeTextEditorSelection?: vscode.Selection;
97100
private subscriptions: vscode.Disposable[] = [];
98101

99102
private disposeEmitter = new vscode.EventEmitter<void>();
@@ -108,11 +111,11 @@ export class DocumentationPreviewEditor implements vscode.Disposable {
108111
this.subscriptions.push(
109112
this.webviewPanel.webview.onDidReceiveMessage(this.receiveMessage, this),
110113
vscode.window.onDidChangeActiveTextEditor(this.handleActiveTextEditorChange, this),
114+
vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange, this),
111115
vscode.workspace.onDidChangeTextDocument(this.handleDocumentChange, this),
112116
this.webviewPanel.onDidDispose(this.dispose, this)
113117
);
114-
// Reveal the editor, but don't change the focus of the active text editor
115-
webviewPanel.reveal(undefined, true);
118+
this.reveal();
116119
}
117120

118121
/** An event that is fired when the Documentation Preview Editor is disposed */
@@ -125,7 +128,8 @@ export class DocumentationPreviewEditor implements vscode.Disposable {
125128
onDidRenderContent = this.renderEmitter.event;
126129

127130
reveal() {
128-
this.webviewPanel.reveal();
131+
// Reveal the editor, but don't change the focus of the active text editor
132+
this.webviewPanel.reveal(undefined, true);
129133
}
130134

131135
dispose() {
@@ -161,82 +165,98 @@ export class DocumentationPreviewEditor implements vscode.Disposable {
161165
return;
162166
}
163167
this.activeTextEditor = activeTextEditor;
168+
this.activeTextEditorSelection = activeTextEditor.selection;
164169
this.convertDocumentation(activeTextEditor);
165170
}
166171

172+
private handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent) {
173+
if (
174+
this.activeTextEditor !== event.textEditor ||
175+
this.activeTextEditorSelection === event.textEditor.selection
176+
) {
177+
return;
178+
}
179+
this.activeTextEditorSelection = event.textEditor.selection;
180+
this.convertDocumentation(event.textEditor);
181+
}
182+
167183
private handleDocumentChange(event: vscode.TextDocumentChangeEvent) {
168184
if (this.activeTextEditor?.document === event.document) {
169185
this.convertDocumentation(this.activeTextEditor);
170186
}
171187
}
172188

173-
private async convertDocumentation(textEditor: vscode.TextEditor): Promise<void> {
174-
const document = textEditor.document;
175-
if (
176-
document.uri.scheme !== "file" ||
177-
!["markdown", "tutorial", "swift"].includes(document.languageId)
178-
) {
179-
this.postMessage({
180-
type: "update-content",
181-
content: {
182-
type: "error",
183-
errorMessage: PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE,
184-
},
185-
});
186-
return;
187-
}
189+
private convertDocumentation = throttle(
190+
async (textEditor: vscode.TextEditor): Promise<void> => {
191+
const document = textEditor.document;
192+
if (
193+
document.uri.scheme !== "file" ||
194+
!["markdown", "tutorial", "swift"].includes(document.languageId)
195+
) {
196+
this.postMessage({
197+
type: "update-content",
198+
content: {
199+
type: "error",
200+
errorMessage: PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE,
201+
},
202+
});
203+
return;
204+
}
188205

189-
try {
190-
const response = await this.context.languageClientManager.useLanguageClient(
191-
async (client): Promise<DocCDocumentationResponse> => {
192-
return await client.sendRequest(DocCDocumentationRequest.type, {
193-
textDocument: {
194-
uri: document.uri.toString(),
195-
},
196-
position: textEditor.selection.start,
197-
});
206+
try {
207+
const response = await this.context.languageClientManager.useLanguageClient(
208+
async (client): Promise<DocCDocumentationResponse> => {
209+
return await client.sendRequest(DocCDocumentationRequest.type, {
210+
textDocument: {
211+
uri: document.uri.toString(),
212+
},
213+
position: textEditor.selection.start,
214+
});
215+
}
216+
);
217+
this.postMessage({
218+
type: "update-content",
219+
content: {
220+
type: "render-node",
221+
renderNode: this.parseRenderNode(response.renderNode),
222+
},
223+
});
224+
} catch (error) {
225+
// Update the preview editor to reflect what error occurred
226+
let livePreviewErrorMessage = "An internal error occurred";
227+
const baseLogErrorMessage = `SourceKit-LSP request "${DocCDocumentationRequest.method}" failed: `;
228+
if (error instanceof ResponseError) {
229+
if (error.code === LSPErrorCodes.RequestCancelled) {
230+
// We can safely ignore cancellations
231+
return undefined;
232+
}
233+
switch (error.code) {
234+
case LSPErrorCodes.RequestFailed:
235+
// RequestFailed response errors can be shown to the user
236+
livePreviewErrorMessage = error.message;
237+
break;
238+
default:
239+
// We should log additional info for other response errors
240+
this.context.outputChannel.log(
241+
baseLogErrorMessage + JSON.stringify(error.toJson(), undefined, 2)
242+
);
243+
break;
244+
}
245+
} else {
246+
this.context.outputChannel.log(baseLogErrorMessage + `${error}`);
198247
}
199-
);
200-
this.postMessage({
201-
type: "update-content",
202-
content: {
203-
type: "render-node",
204-
renderNode: this.parseRenderNode(response.renderNode),
205-
},
206-
});
207-
} catch (error) {
208-
// Update the preview editor to reflect what error occurred
209-
let livePreviewErrorMessage = "An internal error occurred";
210-
const baseLogErrorMessage = `SourceKit-LSP request "${DocCDocumentationRequest.method}" failed: `;
211-
if (error instanceof ResponseError) {
212-
if (error.code === LSPErrorCodes.RequestCancelled) {
213-
// We can safely ignore cancellations
214-
return undefined;
215-
}
216-
switch (error.code) {
217-
case LSPErrorCodes.RequestFailed:
218-
// RequestFailed response errors can be shown to the user
219-
livePreviewErrorMessage = error.message;
220-
break;
221-
default:
222-
// We should log additional info for other response errors
223-
this.context.outputChannel.log(
224-
baseLogErrorMessage + JSON.stringify(error.toJson(), undefined, 2)
225-
);
226-
break;
227-
}
228-
} else {
229-
this.context.outputChannel.log(baseLogErrorMessage + `${error}`);
248+
this.postMessage({
249+
type: "update-content",
250+
content: {
251+
type: "error",
252+
errorMessage: livePreviewErrorMessage,
253+
},
254+
});
230255
}
231-
this.postMessage({
232-
type: "update-content",
233-
content: {
234-
type: "error",
235-
errorMessage: livePreviewErrorMessage,
236-
},
237-
});
238-
}
239-
}
256+
},
257+
100 /* 10 times per second */,
258+
{ trailing: true }
259+
);
240260

241261
private parseRenderNode(content: string): RenderNode {
242262
const renderNode: RenderNode = JSON.parse(content);

0 commit comments

Comments
 (0)