Add documentation for using Milkdown with various frameworks
- Created a new document for using components in Milkdown. - Added a guide for using plugins in Milkdown, including toggling plugins programmatically and listing official plugins. - Introduced a recipe for integrating Milkdown with Angular, including installation steps and component creation. - Added a recipe for using Milkdown with Next.js, detailing installation and component setup. - Created a guide for integrating Milkdown with NuxtJS, including installation and component creation. - Added a comprehensive guide for using Milkdown with React, covering both Crepe and core Milkdown usage. - Introduced a recipe for SolidJS integration with Milkdown, including installation and component creation. - Added a guide for using Milkdown with Svelte, detailing installation and component setup. - Created a comprehensive guide for integrating Milkdown with Vue, covering both Crepe and core Milkdown usage. - Added a recipe for using Milkdown with Vue2, including installation and component creation.
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
CancellationTokenSource,
|
||||
Disposable,
|
||||
Event,
|
||||
EventEmitter,
|
||||
TextDocument,
|
||||
Uri,
|
||||
WebviewPanel,
|
||||
WorkspaceEdit,
|
||||
commands,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import { IVSCodeExtensionContext } from '../../../../../../platform/extContext/common/extensionContext';
|
||||
import { debounce } from '../../../../../../util/common/debounce';
|
||||
import { BasePanelCompletion, ISuggestionsPanel, PanelConfig } from './basePanelTypes';
|
||||
import { Highlighter } from './highlighter';
|
||||
import { getNonce, pluralize } from './utils';
|
||||
|
||||
//import { IPCitationDetail } from '#lib/citationManager';
|
||||
interface IPCitationDetail {
|
||||
license: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface SuggestionsPanelManagerInterface {
|
||||
activeWebviewPanel: BaseSuggestionsPanel<BasePanelCompletion> | undefined;
|
||||
decrementPanelCount(): void;
|
||||
}
|
||||
|
||||
export interface SolutionContent {
|
||||
htmlSnippet: string;
|
||||
citation?: { message: string; url: string };
|
||||
[key: string]: unknown; // Allow additional properties for panel-specific content
|
||||
}
|
||||
|
||||
export interface BaseWebviewMessage {
|
||||
command: string;
|
||||
}
|
||||
|
||||
interface AcceptSolutionMessage extends BaseWebviewMessage {
|
||||
command: 'acceptSolution';
|
||||
solutionIndex: number;
|
||||
}
|
||||
|
||||
interface FocusSolutionMessage extends BaseWebviewMessage {
|
||||
command: 'focusSolution';
|
||||
solutionIndex: number;
|
||||
}
|
||||
|
||||
interface SubmitFeedbackMessage extends BaseWebviewMessage {
|
||||
command: 'submitFeedback';
|
||||
solutionIndex: number;
|
||||
feedback: string;
|
||||
}
|
||||
|
||||
interface RefreshMessage extends BaseWebviewMessage {
|
||||
command: 'refresh';
|
||||
}
|
||||
|
||||
interface WebviewReadyMessage extends BaseWebviewMessage {
|
||||
command: 'webviewReady';
|
||||
}
|
||||
|
||||
export type WebviewMessage =
|
||||
| AcceptSolutionMessage
|
||||
| FocusSolutionMessage
|
||||
| SubmitFeedbackMessage
|
||||
| RefreshMessage
|
||||
| WebviewReadyMessage;
|
||||
|
||||
export abstract class BaseSuggestionsPanel<TPanelCompletion extends BasePanelCompletion> implements ISuggestionsPanel {
|
||||
private _disposables: Disposable[] = [];
|
||||
#items: TPanelCompletion[] = [];
|
||||
#batchItems: TPanelCompletion[] = [];
|
||||
#percentage = 0;
|
||||
#highlighter: Thenable<Highlighter>;
|
||||
private _focusedSolution: TPanelCompletion | undefined;
|
||||
private _isDisposed: boolean = false;
|
||||
#documentUri: Uri;
|
||||
#cts = new CancellationTokenSource();
|
||||
|
||||
private _onDidDispose = new EventEmitter<void>();
|
||||
readonly onDidDispose: Event<void> = this._onDidDispose.event;
|
||||
|
||||
get cancellationToken() {
|
||||
return this.#cts.token;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly webviewPanel: WebviewPanel,
|
||||
document: TextDocument,
|
||||
protected suggestionsPanelManager: SuggestionsPanelManagerInterface,
|
||||
protected readonly config: PanelConfig,
|
||||
@IVSCodeExtensionContext protected readonly contextService: IVSCodeExtensionContext,
|
||||
) {
|
||||
webviewPanel.onDidDispose(() => this._dispose(), null, this._disposables);
|
||||
webviewPanel.webview.html = this._getWebviewContent();
|
||||
this.#documentUri = document.uri;
|
||||
|
||||
this.#highlighter = Highlighter.create(document.languageId);
|
||||
|
||||
workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('workbench.colorTheme')) {
|
||||
return this.render();
|
||||
}
|
||||
});
|
||||
|
||||
webviewPanel.webview.onDidReceiveMessage(async (message: WebviewMessage) => {
|
||||
// First lest the subclass handle custom messages
|
||||
if ((await this.handleCustomMessage(message)) === true) {
|
||||
return;
|
||||
}
|
||||
switch (message.command) {
|
||||
case 'focusSolution':
|
||||
this._focusedSolution = this.#items[message.solutionIndex];
|
||||
return;
|
||||
case 'webviewReady':
|
||||
// Send the config to the webview
|
||||
void this.postMessage({
|
||||
command: 'updateConfig',
|
||||
config: {
|
||||
renderingMode: this.config.renderingMode,
|
||||
shuffleSolutions: this.config.shuffleSolutions,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}, undefined);
|
||||
|
||||
webviewPanel.onDidChangeViewState(e => {
|
||||
if (e.webviewPanel?.visible) {
|
||||
this.suggestionsPanelManager.activeWebviewPanel = this;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async handleCustomMessage(message: BaseWebviewMessage): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
protected abstract renderSolutionContent(item: TPanelCompletion, baseContent: SolutionContent): SolutionContent;
|
||||
|
||||
private _buildExtensionUri(...path: string[]): Uri {
|
||||
const extensionPath = Uri.joinPath(this.contextService.extensionUri, ...path);
|
||||
return this.webviewPanel.webview.asWebviewUri(extensionPath);
|
||||
}
|
||||
|
||||
private _getWebviewContent() {
|
||||
const nonce = getNonce();
|
||||
const scriptUri = this._buildExtensionUri('dist', this.config.webviewScriptName);
|
||||
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; font-src ${this.webviewPanel.webview.cspSource}; style-src 'unsafe-inline' ${this.webviewPanel.webview.cspSource}; script-src 'nonce-${nonce}';"
|
||||
/>
|
||||
<title>${this.config.panelTitle}</title>
|
||||
<style>
|
||||
.solutionHeading {
|
||||
margin-top: 40px;
|
||||
}
|
||||
pre:focus-visible {
|
||||
border: 1px solid var(--vscode-focusBorder);
|
||||
outline: none;
|
||||
}
|
||||
pre {
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
padding: 9.5px;
|
||||
line-height: 1.42857143;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vscode-notebook-cellBorderColor);
|
||||
white-space: pre-wrap;
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
}
|
||||
pre.shiki {
|
||||
padding: 0.5em 0.7em;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
code {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>${this.config.panelTitle}</h2>
|
||||
<div id="loadingContainer" aria-live="assertive" aria-atomic="true">
|
||||
<label for="progress-bar">Loading suggestions:</label>
|
||||
<progress id="progress-bar" max="100" value="0"></progress>
|
||||
</div>
|
||||
<div id="solutionsContainer" aria-busy="true" aria-describedby="progress-bar"></div>
|
||||
<script nonce="${nonce}" type="module" src="${scriptUri.toString()}"></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
onWorkDone({ percentage }: { percentage: number }) {
|
||||
this.#percentage = percentage;
|
||||
void this.render();
|
||||
}
|
||||
|
||||
onItem(item: TPanelCompletion) {
|
||||
// If rendering mode is 'batch', we collect items and render them later
|
||||
// Otherwise, we render immediately
|
||||
if (this.config.renderingMode === 'batch') {
|
||||
this.#batchItems.push(item);
|
||||
} else {
|
||||
this.#items.push(item);
|
||||
void this.render();
|
||||
}
|
||||
}
|
||||
|
||||
clearSolutions() {
|
||||
// Cancel any ongoing operations
|
||||
this.#cts.cancel();
|
||||
// Create a new cancellation token source for the next operation
|
||||
this.#cts = new CancellationTokenSource();
|
||||
|
||||
// Clear all solutions and reset state
|
||||
this.#items = [];
|
||||
this.#batchItems = [];
|
||||
this._focusedSolution = undefined;
|
||||
this.#percentage = 0;
|
||||
void this.render();
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.#percentage = 100;
|
||||
|
||||
// If we have batch items, add them to the main items list, shuffle if needed, and render
|
||||
if (this.#batchItems.length > 0) {
|
||||
this.#items.push(...this.#batchItems);
|
||||
|
||||
if (this.config.shuffleSolutions) {
|
||||
this.#items = this.#items.sort(() => Math.random() - 0.5);
|
||||
}
|
||||
|
||||
this.#batchItems = [];
|
||||
}
|
||||
|
||||
void this.render();
|
||||
}
|
||||
|
||||
protected async acceptSolution(solution: TPanelCompletion, closePanel: boolean = true) {
|
||||
if (this._isDisposed === false && solution?.range) {
|
||||
const edit = new WorkspaceEdit();
|
||||
edit.replace(this.#documentUri, solution.range, solution.insertText);
|
||||
await workspace.applyEdit(edit);
|
||||
this.#cts.cancel();
|
||||
if (closePanel) {
|
||||
await commands.executeCommand('workbench.action.closeActiveEditor');
|
||||
}
|
||||
await solution.postInsertionCallback();
|
||||
}
|
||||
}
|
||||
|
||||
protected items(): TPanelCompletion[] {
|
||||
return this.#items;
|
||||
}
|
||||
|
||||
async acceptFocusedSolution() {
|
||||
const solution = this._focusedSolution;
|
||||
if (solution) {
|
||||
return this.acceptSolution(solution);
|
||||
}
|
||||
}
|
||||
|
||||
protected async renderSolutions() {
|
||||
const highlighter = await this.#highlighter;
|
||||
const content = this.#items.map(item => {
|
||||
const firstCitation = item.copilotAnnotations?.ip_code_citations?.[0];
|
||||
const details = firstCitation?.details.citations as IPCitationDetail[] | undefined;
|
||||
let renderedCitatation: { message: string; url: string } | undefined;
|
||||
if (details && details.length > 0) {
|
||||
const licensesSet = new Set(details.map(d => d.license));
|
||||
if (licensesSet.has('NOASSERTION')) {
|
||||
licensesSet.delete('NOASSERTION');
|
||||
licensesSet.add('unknown');
|
||||
}
|
||||
const allLicenses = Array.from(licensesSet).sort();
|
||||
const licenseString = allLicenses.length === 1 ? allLicenses[0] : `[${allLicenses.join(', ')}]`;
|
||||
renderedCitatation = {
|
||||
message: `Similar code with ${pluralize(allLicenses.length, 'license type')} ${licenseString} detected.`,
|
||||
url: details[0].url,
|
||||
};
|
||||
}
|
||||
|
||||
const baseContent = {
|
||||
htmlSnippet: highlighter.createSnippet(item.insertText.trim()),
|
||||
citation: renderedCitatation,
|
||||
};
|
||||
|
||||
return this.renderSolutionContent(item, baseContent);
|
||||
});
|
||||
|
||||
const message = this.createSolutionsMessage(content, this.#percentage);
|
||||
await this.postMessage(message);
|
||||
}
|
||||
|
||||
// Subclasses must implement this to create their specific message format
|
||||
protected abstract createSolutionsMessage(content: SolutionContent[], percentage: number): unknown;
|
||||
|
||||
render = debounce(10, () => this.renderSolutions());
|
||||
|
||||
postMessage(message: unknown) {
|
||||
if (this._isDisposed === false) {
|
||||
return this.webviewPanel.webview.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private _dispose() {
|
||||
this._isDisposed = true;
|
||||
this._onDidDispose.fire();
|
||||
this.suggestionsPanelManager.decrementPanelCount();
|
||||
while (this._disposables.length) {
|
||||
const disposable = this._disposables.pop();
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
this._onDidDispose.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user