Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rendering New Notebooks Panel using VS Code Task Provider #1784

Merged
merged 7 commits into from
Nov 26, 2024
Merged
12 changes: 3 additions & 9 deletions src/extension/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,9 @@ export class RunmeExtension {
let treeViewer: RunmeTreeProvider

if (kernel.isFeatureOn(FeatureName.NewTreeProvider)) {
await commands.executeCommand('setContext', 'runme.launcher.isExpanded', true)
await commands.executeCommand('setContext', 'runme.launcher.includeUnnamed', true)
treeViewer = new RunmeLauncherProviderBeta(
kernel,
server,
serializer,
getDefaultWorkspace(),
runner,
)
await commands.executeCommand('setContext', 'runme.launcher.isExpanded', false)
await commands.executeCommand('setContext', 'runme.launcher.includeUnnamed', false)
treeViewer = new RunmeLauncherProviderBeta(kernel, serializer)
} else {
treeViewer = new RunmeLauncherProvider(getDefaultWorkspace())
}
Expand Down
14 changes: 7 additions & 7 deletions src/extension/provider/codelens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,15 @@ export class RunmeCodeLensProvider implements CodeLensProvider, Disposable {
break
}

const task = await RunmeTaskProvider.newRunmeTask(
document.uri.fsPath,
getAnnotations(cell.metadata).name,
const task = await RunmeTaskProvider.newRunmeTask({
filePath: document.uri.fsPath,
command: getAnnotations(cell.metadata).name,
notebook,
cell,
{},
this.runner!,
this.kernel?.getRunnerEnvironment(),
)
options: {},
runner: this.runner!,
runnerEnv: this.kernel?.getRunnerEnvironment(),
})

await tasks.executeTask(task)
}
Expand Down
166 changes: 27 additions & 139 deletions src/extension/provider/launcherBeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,15 @@ import {
Uri,
CancellationTokenSource,
NotebookData,
NotebookCell,
NotebookCellData,
commands,
window,
EventEmitter,
tasks as vscodeTasks,
Task,
} from 'vscode'
import { GrpcTransport } from '@protobuf-ts/grpc-transport'
import { ServerStreamingCall } from '@protobuf-ts/runtime-rpc'
import {
firstValueFrom,
from,
isObservable,
lastValueFrom,
map,
Observable,
of,
toArray,
} from 'rxjs'

import { initProjectClient, ProjectServiceClient, ReadyPromise } from '../grpc/client'
import KernelServer from '../server/kernelServer'
import { LoadEventFoundTask, LoadRequest, LoadResponse } from '../grpc/projectTypes'
import { Serializer } from '../../types'
import { RunmeIdentity } from '../grpc/serializerTypes'

import { asWorkspaceRelativePath, getAnnotations } from '../utils'
import { Kernel } from '../kernel'
import type { IRunner } from '../runner'
import getLogger from '../logger'
import { SerializerBase } from '../serializer'
import { LANGID_AND_EXTENSIONS } from '../../constants'
Expand All @@ -44,14 +26,6 @@ import { OpenFileOptions, RunmeFile, RunmeTreeProvider } from './launcher'
export const GLOB_PATTERN = '**/*.{md,mdr,mdx}'
const logger = getLogger('LauncherBeta')

type LoadStream = ServerStreamingCall<LoadRequest, LoadResponse>

type ProjectTask = LoadEventFoundTask

type TaskNotebook = NotebookData | Serializer.Notebook

type TaskCell = NotebookCell | NotebookCellData | Serializer.Cell

/**
* used to force VS Code update the tree view when user expands/collapses all
* see https://github.com/microsoft/vscode/issues/172479
Expand All @@ -62,35 +36,15 @@ let sauceCount = 0
export class RunmeLauncherProvider implements RunmeTreeProvider {
#disposables: Disposable[] = []
private allowUnnamed = false
private tasks: Promise<ProjectTask[]>
private ready: ReadyPromise
private client: ProjectServiceClient | undefined
private defaultItemState = TreeItemCollapsibleState.Expanded
private serverReadyListener: Disposable | undefined
private defaultItemState = TreeItemCollapsibleState.Collapsed
private _onDidChangeTreeData = new EventEmitter<RunmeFile | undefined>()

constructor(
private kernel: Kernel,
private server: KernelServer,
private serializer: SerializerBase,
private workspaceRoot?: string | undefined,
private runner?: IRunner,
) {
const watcher = workspace.createFileSystemWatcher(GLOB_PATTERN, false, true, false)

this.serverReadyListener = this.server.onTransportReady(({ transport }) =>
this.initProjectClient(transport),
)

this.ready = new Promise((resolve) => {
const disposable = server.onTransportReady(() => {
disposable.dispose()
resolve()
})
})

this.tasks = lastValueFrom(this.loadProjectTasks())

this.#disposables.push(
watcher.onDidCreate((file) => logger.info('onDidCreate: ', file.fsPath)),
watcher.onDidDelete((file) => logger.info('onDidDelete: ', file.fsPath)),
Expand Down Expand Up @@ -152,27 +106,30 @@ export class RunmeLauncherProvider implements RunmeTreeProvider {
}

async getChildren(element?: RunmeFile | undefined): Promise<RunmeFile[]> {
const allTasks = await this.tasks
const tasks = await vscodeTasks.fetchTasks({ type: 'runme' })

if (!element) {
/**
* we need to tweak the folder name to force VS Code re-render the tree view
* see https://github.com/microsoft/vscode/issues/172479
*/
++sauceCount
return Promise.resolve(await this.getNotebooks(allTasks))
return Promise.resolve(this.getNotebooks(tasks))
}

return Promise.resolve(await this.getCells(allTasks, element))
return Promise.resolve(await this.getCells(tasks, element))
}

async getNotebooks(tasks: LoadEventFoundTask[]): Promise<RunmeFile[]> {
getNotebooks(tasks: Task[]): RunmeFile[] {
const foundTasks: RunmeFile[] = []
let prevDir: string | undefined

for (const task of tasks) {
const { documentPath } = task
const { definition } = task
const fileUri = definition.fileUri as Uri
const documentPath = fileUri.path
const { outside, relativePath } = asWorkspaceRelativePath(documentPath)

if (outside) {
continue
}
Expand All @@ -195,16 +152,21 @@ export class RunmeLauncherProvider implements RunmeTreeProvider {
return foundTasks
}

async getCells(tasks: LoadEventFoundTask[], element: RunmeFile): Promise<RunmeFile[]> {
async getCells(tasks: Task[], element: RunmeFile): Promise<RunmeFile[]> {
const foundTasks: RunmeFile[] = []

let mdBuffer: Uint8Array
let prevFile: string | undefined
let notebook: Observable<NotebookData> | undefined
let notebook: NotebookData | undefined

for (const task of tasks) {
const { documentPath, name, id, isNameGenerated } = task
const {
name,
definition: { fileUri, isNameGenerated },
} = task
const documentPath = fileUri?.path
const { outside, relativePath } = asWorkspaceRelativePath(documentPath)

if (outside || (!this.allowUnnamed && isNameGenerated)) {
continue
}
Expand All @@ -226,31 +188,20 @@ export class RunmeLauncherProvider implements RunmeTreeProvider {
}

const token = new CancellationTokenSource().token
notebook = from(this.serializer.deserializeNotebook(mdBuffer, token))
notebook = await this.serializer.deserializeNotebook(mdBuffer, token)
}

if (!notebook) {
continue
}

const cell = notebook.pipe(
map((n) => {
return n.cells.find(
(cell) => cell.metadata?.['id'] === id || cell.metadata?.['runme.dev/name'] === name,
)!
}),
)

const notebookish = !isObservable(notebook) ? of(notebook) : notebook
const taskNotebook = await firstValueFrom<TaskNotebook>(notebookish as any)
const cellish = !isObservable(cell) ? of(cell) : cell
const taskCell = await firstValueFrom<TaskCell>(cellish as any)
const cell = notebook.cells.find((cell) => cell.metadata?.['runme.dev/name'] === name)!

const { excludeFromRunAll } = getAnnotations(taskCell.metadata)
const cellText = 'value' in taskCell ? taskCell.value : taskCell.document.getText()
const languageId = ('languageId' in taskCell && taskCell.languageId) || 'sh'
const cellIndex = taskNotebook.cells.findIndex(
(cell) => cell.metadata?.['id'] === id || cell.metadata?.['runme.dev/name'] === name,
const { excludeFromRunAll } = getAnnotations(cell.metadata)
const cellText = 'value' in cell ? cell.value : ''
const languageId = ('languageId' in cell && cell.languageId) || 'sh'
const cellIndex = notebook.cells.findIndex(
(cell) => cell.metadata?.['runme.dev/name'] === name,
)

const lines = cellText.split('\n')
Expand Down Expand Up @@ -285,69 +236,6 @@ export class RunmeLauncherProvider implements RunmeTreeProvider {
this.#disposables.forEach((d) => d.dispose())
}

// Internal

private async initProjectClient(transport?: GrpcTransport) {
this.client = initProjectClient(transport ?? (await this.server.transport()))
}

protected loadProjectTasks(): Observable<ProjectTask[]> {
if (!workspace.workspaceFolders?.length) {
return of([])
}

const separator = workspace.workspaceFolders[0].uri.fsPath.indexOf('/') > -1 ? '/' : '\\'

const requests = (workspace.workspaceFolders ?? []).map((folder) => {
return <LoadRequest>{
kind: {
oneofKind: 'directory',
directory: {
path: workspace.asRelativePath(folder.uri),
skipGitignore: false,
ignoreFilePatterns: [],
skipRepoLookupUpward: false,
},
},
identity: RunmeIdentity.ALL,
}
})

const task$ = new Observable<ProjectTask>((observer) => {
this.ready.then(() =>
Promise.all(
requests.map((request) => {
const session: LoadStream = this.client!.load(request)
session.responses.onMessage((msg) => {
if (msg.data.oneofKind !== 'foundTask') {
return
}
observer.next(msg.data.foundTask)
})
return session
}),
).then(() => {
logger.info('Finished walk.')
observer.complete()
}),
)
})

const dirProx = (pt: ProjectTask) => {
const { relativePath, outside } = asWorkspaceRelativePath(pt.documentPath)
const len = relativePath.split(separator).length
if (outside) {
return 100 * len
}
return len
}

return task$.pipe(
toArray(),
map((tasks) => tasks.sort((a, b) => dirProx(a) - dirProx(b))),
)
}

resolveExtension(languageId: string): string {
const key = languageId.toLowerCase()
return LANGID_AND_EXTENSIONS.get(key) || languageId
Expand Down
57 changes: 42 additions & 15 deletions src/extension/provider/runmeTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ type TaskNotebook = NotebookData | Serializer.Notebook

type TaskCell = NotebookCell | NotebookCellData | Serializer.Cell

type RunmeTaskOptions = {
isNameGenerated?: boolean
filePath: string
command: string
notebook: TaskNotebook | Observable<TaskNotebook>
cell: TaskCell | Observable<TaskCell>
options: TaskOptions
runner: IRunner
runnerEnv: IRunnerEnvironment | undefined
}

export class RunmeTaskProvider implements TaskProvider {
static execCount = 0
static id = 'runme'
Expand Down Expand Up @@ -207,14 +218,14 @@ export class RunmeTaskProvider implements TaskProvider {
}

static async newRunmeProjectTask(
knownTask: Pick<ProjectTask, 'id' | 'name' | 'documentPath'>,
knownTask: Pick<ProjectTask, 'id' | 'name' | 'documentPath' | 'isNameGenerated'>,
options: TaskOptions = {},
token: CancellationToken,
serializer: SerializerBase,
runner: IRunner,
runnerEnv?: IRunnerEnvironment,
): Promise<Task> {
const { id, name, documentPath } = knownTask
const { id, name, documentPath, isNameGenerated } = knownTask
let mdBuffer: Uint8Array
try {
mdBuffer = await workspace.fs.readFile(Uri.parse(documentPath))
Expand All @@ -235,26 +246,42 @@ export class RunmeTaskProvider implements TaskProvider {
}),
)

return this.newRunmeTask(documentPath, name, notebook, cell, options, runner, runnerEnv)
return this.newRunmeTask({
isNameGenerated: isNameGenerated,
filePath: documentPath,
command: name,
notebook: notebook,
cell: cell,
options,
runner,
runnerEnv,
})
}

static async newRunmeTask(
filePath: string,
command: string,
notebookish: TaskNotebook | Observable<TaskNotebook>,
cellish: TaskCell | Observable<TaskCell>,
options: TaskOptions = {},
runner: IRunner,
runnerEnv: IRunnerEnvironment | undefined,
): Promise<Task> {
static async newRunmeTask({
isNameGenerated,
filePath,
command,
notebook,
cell,
options = {},
runner,
runnerEnv,
}: RunmeTaskOptions): Promise<Task> {
const source = asWorkspaceRelativePath(filePath).relativePath
const name = `${command}`

notebookish = !isObservable(notebookish) ? of(notebookish) : notebookish
cellish = !isObservable(cellish) ? of(cellish) : cellish
const notebookish = !isObservable(notebook) ? of(notebook) : notebook
const cellish = !isObservable(cell) ? of(cell) : cell

const task = new Task(
{ type: 'runme', name, command: name },
{
type: 'runme',
name,
command: name,
fileUri: Uri.file(filePath),
isNameGenerated: isNameGenerated,
},
TaskScope.Workspace,
name,
source,
Expand Down