Path: blob/main/extensions/copilot/src/util/common/notebooks.ts
13397 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';6import { Uri } from '../../vscodeTypes';7import * as glob from '../vs/base/common/glob';8import { Schemas } from '../vs/base/common/network';9import { basename } from '../vs/base/common/path';10import { isEqual } from '../vs/base/common/resources';11import { URI } from '../vs/base/common/uri';121314export interface INotebookSection {15title: string;16content: string;17}1819export interface INotebookOutline {20description: string;21sections: INotebookSection[];22}2324export interface INotebookExclusiveDocumentFilter {25include?: string | vscode.RelativePattern;26exclude?: string | vscode.RelativePattern;27}2829export interface INotebookFilenamePattern {30filenamePattern: string;31excludeFileNamePattern?: string;32}3334export type NotebookSelector = vscode.GlobPattern | INotebookExclusiveDocumentFilter | INotebookFilenamePattern;3536export enum RegisteredEditorPriority {37builtin = 'builtin',38option = 'option',39exclusive = 'exclusive',40default = 'default'41}4243export interface INotebookEditorContribution {44readonly type: string;45readonly displayName: string;46readonly priority?: RegisteredEditorPriority;4748selector: NotebookSelector[];49}5051export interface EditorAssociation {52viewType: string;53filenamePattern?: string;54}5556/**57* Find a notebook document by uri or cell uri.58*/59export function findNotebook(uri: vscode.Uri, notebookDocuments: readonly vscode.NotebookDocument[]): vscode.NotebookDocument | undefined {60return notebookDocuments.find(doc => isEqual(doc.uri, uri) || doc.uri.path === uri.path || findCell(uri, doc));61}6263export function findCell(cellUri: vscode.Uri, notebook: vscode.NotebookDocument): vscode.NotebookCell | undefined {64if (cellUri.scheme === Schemas.vscodeNotebookCell || cellUri.scheme === Schemas.vscodeNotebookCellOutput) {65// Fragment is not unique to a notebook, hence ensure we compaure the path as well.66const index = notebook.getCells().findIndex(cell => isEqual(cell.document.uri, cellUri) || (cell.document.uri.fragment === cellUri.fragment && cell.document.uri.path === cellUri.path));67if (index !== -1) {68return notebook.getCells()[index];69}70}71}727374export function getNotebookCellOutput(outputUri: Uri, notebookDocuments: readonly vscode.NotebookDocument[]): [vscode.NotebookDocument, vscode.NotebookCell, vscode.NotebookCellOutput] | undefined {75if (outputUri.scheme !== Schemas.vscodeNotebookCellOutput) {76return undefined;77}78const params = new URLSearchParams(outputUri.query);79const [notebook, cell] = getNotebookAndCellFromUri(outputUri, notebookDocuments);80if (!cell || !cell.outputs.length) {81return undefined;82}83const outputIndex = (params.get('outputIndex') ? parseInt(params.get('outputIndex') || '', 10) : undefined) || 0;84if (outputIndex > (cell.outputs.length - 1)) {85return;86}87return [notebook, cell, cell.outputs[outputIndex]] as const;88}8990export function getNotebookAndCellFromUri(uri: Uri, notebookDocuments: readonly vscode.NotebookDocument[]): [undefined, undefined] | [vscode.NotebookDocument, vscode.NotebookCell | undefined] {91const notebook = findNotebook(uri, notebookDocuments) || notebookDocuments.find(doc => doc.uri.path === uri.path);92if (!notebook) {93return [undefined, undefined];94}95const cell = findCell(uri, notebook);96if (cell === undefined) {97// Possible the cell has since been deleted.98return [notebook, undefined];99}100return [notebook, cell];101}102103export function isNotebookCellOrNotebookChatInput(uri: vscode.Uri): boolean {104return uri.scheme === Schemas.vscodeNotebookCell105// Support the experimental cell chat widget106|| (uri.scheme === 'untitled' && uri.fragment.startsWith('notebook-chat-input'));107}108109export function isNotebookCell(uri: vscode.Uri): boolean {110return uri.scheme === Schemas.vscodeNotebookCell;111}112113export function isJupyterNotebookUri(uri: vscode.Uri): boolean {114return uri.path.endsWith('.ipynb');115}116117export function isJupyterNotebook(notebook: vscode.NotebookDocument): boolean {118return notebook.notebookType === 'jupyter-notebook';119}120121122export function serializeNotebookDocument(document: vscode.NotebookDocument, features: { cell_uri_fragment?: boolean } = {}): string {123return JSON.stringify({124cells: document.getCells().map(cell => ({125uri_fragment: features.cell_uri_fragment ? cell.document.uri.fragment : undefined,126cell_type: cell.kind,127source: cell.document.getText().split(/\r?\n/),128}))129});130}131132export function extractNotebookOutline(response: string): INotebookOutline | undefined {133try {134const trimmedResponse = response.replace(/\n/g, '');135const regex = /```(?:json)?(.+)/g;136const match = regex.exec(trimmedResponse);137if (match) {138const prefixTrimed = match[1];139// remove content after ```140const suffixBacktick = prefixTrimed.indexOf('```');141const json = suffixBacktick === -1 ? prefixTrimed : prefixTrimed.substring(0, suffixBacktick);142return JSON.parse(json) as INotebookOutline;143}144} catch (ex) { }145146return undefined;147}148149/**150* Checks if the provided pattern is a document exclude pattern151*/152export function isDocumentExcludePattern(pattern: string | vscode.RelativePattern | INotebookExclusiveDocumentFilter | INotebookFilenamePattern): pattern is INotebookExclusiveDocumentFilter {153const arg = pattern as INotebookExclusiveDocumentFilter;154155// Check if it has include property (exclude is optional)156return typeof arg === 'object' && arg !== null &&157(typeof arg.include === 'string' || isRelativePattern(arg.include));158}159160/**161* Checks if the provided pattern is a filename pattern162*/163export function isFilenamePattern(pattern: string | vscode.RelativePattern | INotebookExclusiveDocumentFilter | INotebookFilenamePattern): pattern is INotebookFilenamePattern {164const arg = pattern as INotebookFilenamePattern;165166// Check if it has filenamePattern property167return typeof arg === 'object' && arg !== null && typeof arg.filenamePattern === 'string';168}169170/**a171* Checks if the provided object is a RelativePattern172*/173export function isRelativePattern(obj: unknown): obj is vscode.RelativePattern {174const rp = obj as vscode.RelativePattern | undefined | null;175if (!rp) {176return false;177}178179return typeof rp.base === 'string' && typeof rp.pattern === 'string';180}181182/**183* Checks if the provided object is a valid INotebookEditorContribution184*/185export function isNotebookEditorContribution(contrib: unknown): contrib is INotebookEditorContribution {186const candidate = contrib as INotebookEditorContribution | undefined;187return !!candidate && !!candidate.type && !!candidate.displayName && !!candidate.selector;188}189190/**191* Extracts editor associations from the raw editor association config object192*193* @param raw The raw editor association config object194* @returns An array of EditorAssociation objects195*/196export function extractEditorAssociation(raw: { [fileNamePattern: string]: string }): EditorAssociation[] {197const associations: EditorAssociation[] = [];198for (const [filenamePattern, viewType] of Object.entries(raw)) {199if (viewType) {200associations.push({ filenamePattern, viewType });201}202}203return associations;204}205206/**207* Checks if a resource matches a selector208*/209export function notebookSelectorMatches(resource: URI, selector: NotebookSelector): boolean {210if (typeof selector === 'string') {211// selector as string212if (glob.match(selector.toLowerCase(), basename(resource.fsPath).toLowerCase())) {213return true;214}215}216217if (isDocumentExcludePattern(selector)) {218// selector as INotebookExclusiveDocumentFilter219const filenamePattern = selector.include;220const excludeFilenamePattern = selector.exclude;221222if (!filenamePattern) {223return false;224}225226if (glob.match(filenamePattern, basename(resource.fsPath).toLowerCase())) {227if (excludeFilenamePattern && glob.match(excludeFilenamePattern, basename(resource.fsPath).toLowerCase())) {228return false;229}230return true;231}232}233234if (isFilenamePattern(selector)) {235// selector as INotebookFilenamePattern236if (glob.match(selector.filenamePattern, basename(resource.fsPath).toLowerCase())) {237if (selector.excludeFileNamePattern && glob.match(selector.excludeFileNamePattern, basename(resource.fsPath).toLowerCase())) {238return false;239}240return true;241}242}243244return false;245}246247/**248* Returns all associations that match the glob of the provided resource249*/250export function getNotebookEditorAssociations(resource: Uri, editorAssociations: EditorAssociation[]): EditorAssociation[] {251const validAssociations: EditorAssociation[] = [];252for (const a of editorAssociations) {253if (a.filenamePattern && glob.match(a.filenamePattern.toLowerCase(), basename(resource.fsPath).toLowerCase())) {254validAssociations.push({ filenamePattern: a.filenamePattern, viewType: a.viewType });255}256}257258return validAssociations;259}260261/**262* Checks if the provided resource has a supported notebook provider263*/264export function _hasSupportedNotebooks(uri: Uri, workspaceNotebookDocuments: readonly vscode.NotebookDocument[], notebookEditorContributions: INotebookEditorContribution[], editorAssociations: EditorAssociation[]): boolean {265if (findNotebook(uri, workspaceNotebookDocuments)) {266return true;267}268269const validNotebookEditorContribs: INotebookEditorContribution[] = notebookEditorContributions.filter(notebookEditorContrib => notebookEditorContrib.selector.some(selector => notebookSelectorMatches(uri, selector)));270if (validNotebookEditorContribs.length === 0) {271return false;272}273274const validAssociations = getNotebookEditorAssociations(uri, editorAssociations);275for (const association of validAssociations) {276if (validNotebookEditorContribs.some(notebookEditorContrib => notebookEditorContrib.type === association.viewType)) {277return true;278}279}280281// often users won't have associations that take priority, so check the priority of our valid providers282// a provider with priority !default will only be chosen if there is an association that matches, so we need default at this point283// In VS Code, if priority is empty, it defaults to `default`, vscode/main/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts#L110284if (validNotebookEditorContribs.some(notebookEditorContrib => (notebookEditorContrib.priority ?? RegisteredEditorPriority.default) === RegisteredEditorPriority.default)) {285return true;286} else {287return false;288}289}290291292