From 614e0571d4b009c5b1d38332e2e0748f31442f8c Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 18 Dec 2024 17:02:42 -0500 Subject: [PATCH 1/3] throttle convert requests to 10Hz --- package-lock.json | 47 ++++++ package.json | 2 + .../DocumentationPreviewEditor.ts | 134 +++++++++--------- 3 files changed, 119 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a0e543a0..9fd05d3c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@types/chai-subset": "^1.3.5", "@types/glob": "^7.1.6", "@types/lcov-parse": "^1.0.2", + "@types/lodash.throttle": "^4.1.9", "@types/mocha": "^10.0.10", "@types/mock-fs": "^4.13.4", "@types/node": "^18.19.76", @@ -43,6 +44,7 @@ "esbuild": "^0.25.0", "eslint": "^8.57.0", "eslint-config-prettier": "^10.0.1", + "lodash.throttle": "^4.1.1", "mocha": "^10.8.2", "mock-fs": "^5.5.0", "node-pty": "^1.0.0", @@ -1106,6 +1108,23 @@ "integrity": "sha512-tdoxiYm04XdDEdR7UMwkWj78UAVo9U2IOcxI6tmX2/s9TK/ue/9T8gbpS/07yeWyVkVO0UumFQ5EUIBQbVejzQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.throttle": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", + "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -4099,6 +4118,13 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -6777,6 +6803,21 @@ "integrity": "sha512-tdoxiYm04XdDEdR7UMwkWj78UAVo9U2IOcxI6tmX2/s9TK/ue/9T8gbpS/07yeWyVkVO0UumFQ5EUIBQbVejzQ==", "dev": true }, + "@types/lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", + "dev": true + }, + "@types/lodash.throttle": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/lodash.throttle/-/lodash.throttle-4.1.9.tgz", + "integrity": "sha512-PCPVfpfueguWZQB7pJQK890F2scYKoDUL3iM522AptHWn7d5NQmeS/LTEHIcLr5PaTzl3dK2Z0xSUHHTHwaL5g==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -8958,6 +8999,12 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "dev": true }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", diff --git a/package.json b/package.json index 9101e61f0..656b96250 100644 --- a/package.json +++ b/package.json @@ -1566,6 +1566,7 @@ "@types/chai-subset": "^1.3.5", "@types/glob": "^7.1.6", "@types/lcov-parse": "^1.0.2", + "@types/lodash.throttle": "^4.1.9", "@types/mocha": "^10.0.10", "@types/mock-fs": "^4.13.4", "@types/node": "^18.19.76", @@ -1588,6 +1589,7 @@ "esbuild": "^0.25.0", "eslint": "^8.57.0", "eslint-config-prettier": "^10.0.1", + "lodash.throttle": "^4.1.1", "mocha": "^10.8.2", "mock-fs": "^5.5.0", "node-pty": "^1.0.0", diff --git a/src/documentation/DocumentationPreviewEditor.ts b/src/documentation/DocumentationPreviewEditor.ts index d9cd177e8..93581ff80 100644 --- a/src/documentation/DocumentationPreviewEditor.ts +++ b/src/documentation/DocumentationPreviewEditor.ts @@ -19,6 +19,8 @@ import { RenderNode, WebviewContent, WebviewMessage } from "./webview/WebviewMes import { WorkspaceContext } from "../WorkspaceContext"; import { DocCDocumentationRequest, DocCDocumentationResponse } from "../sourcekit-lsp/extensions"; import { LSPErrorCodes, ResponseError } from "vscode-languageclient"; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import throttle = require("lodash.throttle"); export enum PreviewEditorConstant { VIEW_TYPE = "swift.previewDocumentationEditor", @@ -170,73 +172,77 @@ export class DocumentationPreviewEditor implements vscode.Disposable { } } - private async convertDocumentation(textEditor: vscode.TextEditor): Promise { - const document = textEditor.document; - if ( - document.uri.scheme !== "file" || - !["markdown", "tutorial", "swift"].includes(document.languageId) - ) { - this.postMessage({ - type: "update-content", - content: { - type: "error", - errorMessage: PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE, - }, - }); - return; - } + private convertDocumentation = throttle( + async (textEditor: vscode.TextEditor): Promise => { + const document = textEditor.document; + if ( + document.uri.scheme !== "file" || + !["markdown", "tutorial", "swift"].includes(document.languageId) + ) { + this.postMessage({ + type: "update-content", + content: { + type: "error", + errorMessage: PreviewEditorConstant.UNSUPPORTED_EDITOR_ERROR_MESSAGE, + }, + }); + return; + } - try { - const response = await this.context.languageClientManager.useLanguageClient( - async (client): Promise => { - return await client.sendRequest(DocCDocumentationRequest.type, { - textDocument: { - uri: document.uri.toString(), - }, - position: textEditor.selection.start, - }); - } - ); - this.postMessage({ - type: "update-content", - content: { - type: "render-node", - renderNode: this.parseRenderNode(response.renderNode), - }, - }); - } catch (error) { - // Update the preview editor to reflect what error occurred - let livePreviewErrorMessage = "An internal error occurred"; - const baseLogErrorMessage = `SourceKit-LSP request "${DocCDocumentationRequest.method}" failed: `; - if (error instanceof ResponseError) { - if (error.code === LSPErrorCodes.RequestCancelled) { - // We can safely ignore cancellations - return undefined; + try { + const response = await this.context.languageClientManager.useLanguageClient( + async (client): Promise => { + return await client.sendRequest(DocCDocumentationRequest.type, { + textDocument: { + uri: document.uri.toString(), + }, + position: textEditor.selection.start, + }); + } + ); + this.postMessage({ + type: "update-content", + content: { + type: "render-node", + renderNode: this.parseRenderNode(response.renderNode), + }, + }); + } catch (error) { + // Update the preview editor to reflect what error occurred + let livePreviewErrorMessage = "An internal error occurred"; + const baseLogErrorMessage = `SourceKit-LSP request "${DocCDocumentationRequest.method}" failed: `; + if (error instanceof ResponseError) { + if (error.code === LSPErrorCodes.RequestCancelled) { + // We can safely ignore cancellations + return undefined; + } + switch (error.code) { + case LSPErrorCodes.RequestFailed: + // RequestFailed response errors can be shown to the user + livePreviewErrorMessage = error.message; + break; + default: + // We should log additional info for other response errors + this.context.outputChannel.log( + baseLogErrorMessage + JSON.stringify(error.toJson(), undefined, 2) + ); + break; + } + } else { + this.context.outputChannel.log(baseLogErrorMessage + `${error}`); } - switch (error.code) { - case LSPErrorCodes.RequestFailed: - // RequestFailed response errors can be shown to the user - livePreviewErrorMessage = error.message; - break; - default: - // We should log additional info for other response errors - this.context.outputChannel.log( - baseLogErrorMessage + JSON.stringify(error.toJson(), undefined, 2) - ); - break; - } - } else { - this.context.outputChannel.log(baseLogErrorMessage + `${error}`); + this.postMessage({ + type: "update-content", + content: { + type: "error", + errorMessage: livePreviewErrorMessage, + }, + }); } - this.postMessage({ - type: "update-content", - content: { - type: "error", - errorMessage: livePreviewErrorMessage, - }, - }); - } - } + }, + 100 /* 10 times per second */, + { trailing: true } + ); private parseRenderNode(content: string): RenderNode { const renderNode: RenderNode = JSON.parse(content); From 8b52e1fb8286ca2f33adc6b5cfea02496923f8b4 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Wed, 18 Dec 2024 17:05:57 -0500 Subject: [PATCH 2/3] never steal focus when revealing the live preview editor --- src/documentation/DocumentationPreviewEditor.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/documentation/DocumentationPreviewEditor.ts b/src/documentation/DocumentationPreviewEditor.ts index 93581ff80..7d60f5c92 100644 --- a/src/documentation/DocumentationPreviewEditor.ts +++ b/src/documentation/DocumentationPreviewEditor.ts @@ -113,8 +113,7 @@ export class DocumentationPreviewEditor implements vscode.Disposable { vscode.workspace.onDidChangeTextDocument(this.handleDocumentChange, this), this.webviewPanel.onDidDispose(this.dispose, this) ); - // Reveal the editor, but don't change the focus of the active text editor - webviewPanel.reveal(undefined, true); + this.reveal(); } /** An event that is fired when the Documentation Preview Editor is disposed */ @@ -127,7 +126,8 @@ export class DocumentationPreviewEditor implements vscode.Disposable { onDidRenderContent = this.renderEmitter.event; reveal() { - this.webviewPanel.reveal(); + // Reveal the editor, but don't change the focus of the active text editor + this.webviewPanel.reveal(undefined, true); } dispose() { From 3da633b42285f142ea9d2b59169977e7cbdac253 Mon Sep 17 00:00:00 2001 From: Matthew Bastien Date: Thu, 27 Feb 2025 16:40:09 -0500 Subject: [PATCH 3/3] add selection even listener to detect cursor movement --- src/documentation/DocumentationPreviewEditor.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/documentation/DocumentationPreviewEditor.ts b/src/documentation/DocumentationPreviewEditor.ts index 7d60f5c92..2b2a8fb2f 100644 --- a/src/documentation/DocumentationPreviewEditor.ts +++ b/src/documentation/DocumentationPreviewEditor.ts @@ -96,6 +96,7 @@ export class DocumentationPreviewEditor implements vscode.Disposable { } private activeTextEditor?: vscode.TextEditor; + private activeTextEditorSelection?: vscode.Selection; private subscriptions: vscode.Disposable[] = []; private disposeEmitter = new vscode.EventEmitter(); @@ -110,6 +111,7 @@ export class DocumentationPreviewEditor implements vscode.Disposable { this.subscriptions.push( this.webviewPanel.webview.onDidReceiveMessage(this.receiveMessage, this), vscode.window.onDidChangeActiveTextEditor(this.handleActiveTextEditorChange, this), + vscode.window.onDidChangeTextEditorSelection(this.handleSelectionChange, this), vscode.workspace.onDidChangeTextDocument(this.handleDocumentChange, this), this.webviewPanel.onDidDispose(this.dispose, this) ); @@ -163,9 +165,21 @@ export class DocumentationPreviewEditor implements vscode.Disposable { return; } this.activeTextEditor = activeTextEditor; + this.activeTextEditorSelection = activeTextEditor.selection; this.convertDocumentation(activeTextEditor); } + private handleSelectionChange(event: vscode.TextEditorSelectionChangeEvent) { + if ( + this.activeTextEditor !== event.textEditor || + this.activeTextEditorSelection === event.textEditor.selection + ) { + return; + } + this.activeTextEditorSelection = event.textEditor.selection; + this.convertDocumentation(event.textEditor); + } + private handleDocumentChange(event: vscode.TextDocumentChangeEvent) { if (this.activeTextEditor?.document === event.document) { this.convertDocumentation(this.activeTextEditor);