|
| 1 | +import * as vscode from 'vscode'; |
| 2 | +import { getParserForDocument } from './parsers/AppHostResourceParser'; |
| 3 | +// Import parsers to trigger self-registration |
| 4 | +import './parsers/csharpAppHostParser'; |
| 5 | +import './parsers/jsTsAppHostParser'; |
| 6 | +import { AspireAppHostTreeProvider } from '../views/AspireAppHostTreeProvider'; |
| 7 | +import { ResourceJson, AppHostDisplayInfo, ResourceCommandJson } from '../views/AppHostDataRepository'; |
| 8 | +import { findResourceState, findWorkspaceResourceState } from './resourceStateUtils'; |
| 9 | +import { ResourceState, HealthStatus, StateStyle, ResourceType } from './resourceConstants'; |
| 10 | +import { |
| 11 | + codeLensDebugPipelineStep, |
| 12 | + codeLensResourceRunning, |
| 13 | + codeLensResourceRunningWarning, |
| 14 | + codeLensResourceRunningError, |
| 15 | + codeLensResourceStarting, |
| 16 | + codeLensResourceStopped, |
| 17 | + codeLensResourceStoppedError, |
| 18 | + codeLensResourceError, |
| 19 | + codeLensRestart, |
| 20 | + codeLensStop, |
| 21 | + codeLensStart, |
| 22 | + codeLensViewLogs, |
| 23 | + codeLensCommand, |
| 24 | +} from '../loc/strings'; |
| 25 | + |
| 26 | +export class AspireCodeLensProvider implements vscode.CodeLensProvider { |
| 27 | + private readonly _onDidChangeCodeLenses = new vscode.EventEmitter<void>(); |
| 28 | + readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event; |
| 29 | + |
| 30 | + private _disposables: vscode.Disposable[] = []; |
| 31 | + |
| 32 | + constructor(private readonly _treeProvider: AspireAppHostTreeProvider) { |
| 33 | + // Re-compute lenses whenever the polling data changes |
| 34 | + this._disposables.push( |
| 35 | + _treeProvider.onDidChangeTreeData(() => this._onDidChangeCodeLenses.fire()) |
| 36 | + ); |
| 37 | + } |
| 38 | + |
| 39 | + provideCodeLenses(document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.CodeLens[] { |
| 40 | + if (!vscode.workspace.getConfiguration('aspire').get<boolean>('enableCodeLens', true)) { |
| 41 | + return []; |
| 42 | + } |
| 43 | + |
| 44 | + const parser = getParserForDocument(document); |
| 45 | + if (!parser) { |
| 46 | + return []; |
| 47 | + } |
| 48 | + |
| 49 | + const resources = parser.parseResources(document); |
| 50 | + if (resources.length === 0) { |
| 51 | + return []; |
| 52 | + } |
| 53 | + |
| 54 | + const appHosts = this._treeProvider.appHosts; |
| 55 | + const workspaceResources = this._treeProvider.workspaceResources; |
| 56 | + const workspaceAppHostPath = this._treeProvider.workspaceAppHostPath ?? ''; |
| 57 | + const hasRunningData = appHosts.length > 0 || workspaceResources.length > 0; |
| 58 | + const findWorkspace = findWorkspaceResourceState(workspaceResources, workspaceAppHostPath); |
| 59 | + |
| 60 | + const lenses: vscode.CodeLens[] = []; |
| 61 | + |
| 62 | + for (const resource of resources) { |
| 63 | + // Use statementStartLine to position the CodeLens at the top of a multi-line chain |
| 64 | + const lensLine = resource.statementStartLine ?? resource.range.start.line; |
| 65 | + const lineRange = new vscode.Range(lensLine, 0, lensLine, 0); |
| 66 | + |
| 67 | + if (resource.kind === 'pipelineStep') { |
| 68 | + // Pipeline steps get Debug lens when no AppHost is running |
| 69 | + if (!hasRunningData) { |
| 70 | + this._addPipelineStepLenses(lenses, lineRange, resource.name); |
| 71 | + } |
| 72 | + } else if (resource.kind === 'resource') { |
| 73 | + // Resources get state lenses when live data is available |
| 74 | + if (hasRunningData) { |
| 75 | + const match = findResourceState(appHosts, resource.name) |
| 76 | + ?? findWorkspace(resource.name); |
| 77 | + if (match) { |
| 78 | + this._addStateLenses(lenses, lineRange, match.resource, match.appHost); |
| 79 | + } |
| 80 | + } |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + return lenses; |
| 85 | + } |
| 86 | + |
| 87 | + private _addPipelineStepLenses(lenses: vscode.CodeLens[], range: vscode.Range, stepName: string): void { |
| 88 | + lenses.push(new vscode.CodeLens(range, { |
| 89 | + title: codeLensDebugPipelineStep, |
| 90 | + command: 'aspire-vscode.codeLensDebugPipelineStep', |
| 91 | + tooltip: codeLensDebugPipelineStep, |
| 92 | + arguments: [stepName], |
| 93 | + })); |
| 94 | + } |
| 95 | + |
| 96 | + private _addStateLenses( |
| 97 | + lenses: vscode.CodeLens[], |
| 98 | + range: vscode.Range, |
| 99 | + resource: ResourceJson, |
| 100 | + appHost: AppHostDisplayInfo, |
| 101 | + ): void { |
| 102 | + const state = resource.state ?? ''; |
| 103 | + const stateStyle = resource.stateStyle ?? ''; |
| 104 | + const healthStatus = resource.healthStatus; |
| 105 | + const commands = resource.commands ? Object.keys(resource.commands) : []; |
| 106 | + |
| 107 | + // State indicator lens (clickable — reveals resource in tree view) |
| 108 | + let stateLabel = getCodeLensStateLabel(state, stateStyle); |
| 109 | + if (healthStatus && healthStatus !== HealthStatus.Healthy) { |
| 110 | + stateLabel += ` - (${healthStatus})`; |
| 111 | + } |
| 112 | + lenses.push(new vscode.CodeLens(range, { |
| 113 | + title: stateLabel, |
| 114 | + command: 'aspire-vscode.codeLensRevealResource', |
| 115 | + tooltip: `${resource.displayName ?? resource.name}: ${state}${healthStatus ? ` (${healthStatus})` : ''}`, |
| 116 | + arguments: [resource.displayName ?? resource.name], |
| 117 | + })); |
| 118 | + |
| 119 | + // Action lenses based on available commands |
| 120 | + if (commands.includes('restart') || commands.includes('resource-restart')) { |
| 121 | + lenses.push(new vscode.CodeLens(range, { |
| 122 | + title: codeLensRestart, |
| 123 | + command: 'aspire-vscode.codeLensResourceAction', |
| 124 | + tooltip: codeLensRestart, |
| 125 | + arguments: [resource.name, 'restart', appHost.appHostPath], |
| 126 | + })); |
| 127 | + } |
| 128 | + |
| 129 | + if (commands.includes('stop') || commands.includes('resource-stop')) { |
| 130 | + lenses.push(new vscode.CodeLens(range, { |
| 131 | + title: codeLensStop, |
| 132 | + command: 'aspire-vscode.codeLensResourceAction', |
| 133 | + tooltip: codeLensStop, |
| 134 | + arguments: [resource.name, 'stop', appHost.appHostPath], |
| 135 | + })); |
| 136 | + } |
| 137 | + |
| 138 | + if (commands.includes('start') || commands.includes('resource-start')) { |
| 139 | + lenses.push(new vscode.CodeLens(range, { |
| 140 | + title: codeLensStart, |
| 141 | + command: 'aspire-vscode.codeLensResourceAction', |
| 142 | + tooltip: codeLensStart, |
| 143 | + arguments: [resource.name, 'start', appHost.appHostPath], |
| 144 | + })); |
| 145 | + } |
| 146 | + |
| 147 | + // View Logs lens (not applicable to parameters) |
| 148 | + if (resource.resourceType !== ResourceType.Parameter) { |
| 149 | + lenses.push(new vscode.CodeLens(range, { |
| 150 | + title: codeLensViewLogs, |
| 151 | + command: 'aspire-vscode.codeLensViewLogs', |
| 152 | + tooltip: codeLensViewLogs, |
| 153 | + arguments: [resource.displayName ?? resource.name, appHost.appHostPath], |
| 154 | + })); |
| 155 | + } |
| 156 | + |
| 157 | + // Custom commands (non-standard ones like "Reset Database") |
| 158 | + const standardCommands = new Set(['restart', 'resource-restart', 'stop', 'resource-stop', 'start', 'resource-start']); |
| 159 | + if (resource.commands) { |
| 160 | + for (const [cmdName, cmd] of Object.entries(resource.commands) as [string, ResourceCommandJson][]) { |
| 161 | + if (!standardCommands.has(cmdName)) { |
| 162 | + const label = codeLensCommand(cmd.description ?? cmdName); |
| 163 | + lenses.push(new vscode.CodeLens(range, { |
| 164 | + title: label, |
| 165 | + command: 'aspire-vscode.codeLensResourceAction', |
| 166 | + tooltip: cmd.description ?? cmdName, |
| 167 | + arguments: [resource.name, cmdName, appHost.appHostPath], |
| 168 | + })); |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + dispose(): void { |
| 175 | + this._disposables.forEach(d => d.dispose()); |
| 176 | + this._onDidChangeCodeLenses.dispose(); |
| 177 | + } |
| 178 | +} |
| 179 | + |
| 180 | +export function getCodeLensStateLabel(state: string, stateStyle: string): string { |
| 181 | + switch (state) { |
| 182 | + case ResourceState.Running: |
| 183 | + case ResourceState.Active: |
| 184 | + if (stateStyle === StateStyle.Error) { |
| 185 | + return codeLensResourceRunningError; |
| 186 | + } |
| 187 | + if (stateStyle === StateStyle.Warning) { |
| 188 | + return codeLensResourceRunningWarning; |
| 189 | + } |
| 190 | + return codeLensResourceRunning; |
| 191 | + case ResourceState.Starting: |
| 192 | + case ResourceState.Building: |
| 193 | + case ResourceState.Waiting: |
| 194 | + case ResourceState.NotStarted: |
| 195 | + return codeLensResourceStarting; |
| 196 | + case ResourceState.FailedToStart: |
| 197 | + case ResourceState.RuntimeUnhealthy: |
| 198 | + return codeLensResourceError; |
| 199 | + case ResourceState.Finished: |
| 200 | + case ResourceState.Exited: |
| 201 | + case ResourceState.Stopping: |
| 202 | + if (stateStyle === StateStyle.Error) { |
| 203 | + return codeLensResourceStoppedError; |
| 204 | + } |
| 205 | + return codeLensResourceStopped; |
| 206 | + default: |
| 207 | + return state || codeLensResourceStopped; |
| 208 | + } |
| 209 | +} |
0 commit comments