Skip to content

Commit ada82e4

Browse files
author
Nathan Turinski
committed
Refactor into individual tree items
1 parent 3a35411 commit ada82e4

File tree

9 files changed

+247
-227
lines changed

9 files changed

+247
-227
lines changed

package.json

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -485,12 +485,6 @@
485485
"category": "Azure Functions",
486486
"icon": "$(refresh)"
487487
},
488-
{
489-
"command": "azureFunctions.funcHostDebug.clearErrors",
490-
"title": "%azureFunctions.funcHostDebug.clearErrors%",
491-
"category": "Azure Functions",
492-
"icon": "$(clear-all)"
493-
},
494488
{
495489
"command": "azureFunctions.funcHostDebug.showRecentLogs",
496490
"title": "%azureFunctions.funcHostDebug.showRecentLogs%",
@@ -556,11 +550,6 @@
556550
"when": "view == azureFunctions.funcHostDebugView",
557551
"group": "navigation@1"
558552
},
559-
{
560-
"command": "azureFunctions.funcHostDebug.clearErrors",
561-
"when": "view == azureFunctions.funcHostDebugView",
562-
"group": "navigation@1"
563-
},
564553
{
565554
"command": "azureFunctions.funcHostDebug.clearStoppedSessions",
566555
"when": "view == azureFunctions.funcHostDebugView",

package.nls.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@
147147
"azureFunctions.mcpProjectType.SelfHostedMcpServer": "Runs the Functions host in custom-handler mode, forwarding requests to a self-hosted MCP server process.",
148148
"azureFunctions.funcHostDebugView.title": "Functions Host Exceptions Debugger",
149149
"azureFunctions.funcHostDebug.refresh": "Refresh",
150-
"azureFunctions.funcHostDebug.clearErrors": "Clear Function Host Errors",
151150
"azureFunctions.funcHostDebug.showRecentLogs": "Show Recent Host Logs",
152151
"azureFunctions.funcHostDebug.copyRecentLogs": "Copy Recent Host Logs",
153152
"azureFunctions.funcHostDebug.askCopilot": "Ask Copilot",

src/debug/FunctionHostDebugView.ts

Lines changed: 16 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -4,118 +4,18 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { runningFuncTaskMap, stoppedFuncTasks, type IStoppedFuncTask } from '../funcCoreTools/funcHostTask';
8-
import { localize } from '../localize';
7+
import { runningFuncTaskMap, stoppedFuncTasks } from '../funcCoreTools/funcHostTask';
8+
import { HostErrorNode } from './nodes/HostErrorNode';
9+
import { HostTaskNode } from './nodes/HostTaskNode';
10+
import { NoHostNode } from './nodes/NoHostNode';
11+
import { StoppedHostNode } from './nodes/StoppedHostNode';
912

10-
enum FuncHostDebugContextValue {
11-
HostTask = 'azFunc.funcHostDebug.hostTask',
12-
StoppedHostTask = 'azFunc.funcHostDebug.stoppedHostTask',
13-
HostError = 'azFunc.funcHostDebug.hostError',
14-
}
15-
16-
type FuncHostDebugNode = INoHostNode | IHostTaskNode | IStoppedHostNode | IHostErrorNode;
13+
export { getScopeLabel } from './nodes/funcHostDebugUtils';
14+
export { HostErrorNode } from './nodes/HostErrorNode';
15+
export { HostTaskNode } from './nodes/HostTaskNode';
16+
export { StoppedHostNode } from './nodes/StoppedHostNode';
1717

18-
interface INoHostNode {
19-
kind: 'noHost';
20-
}
21-
22-
export interface IHostTaskNode {
23-
kind: 'hostTask';
24-
workspaceFolder: vscode.WorkspaceFolder | vscode.TaskScope;
25-
cwd?: string;
26-
portNumber: string;
27-
startTime: Date;
28-
}
29-
30-
export interface IStoppedHostNode {
31-
kind: 'stoppedHost';
32-
stoppedTask: IStoppedFuncTask;
33-
}
34-
35-
export interface IHostErrorNode {
36-
kind: 'hostError';
37-
workspaceFolder: vscode.WorkspaceFolder | vscode.TaskScope;
38-
cwd?: string;
39-
portNumber: string;
40-
message: string;
41-
}
42-
43-
function formatTimestamp(date: Date): string {
44-
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
45-
}
46-
47-
function buildHostTooltip(opts: { label: string; scopeLabel: string; portNumber: string; startTime: Date; stopTime?: Date; cwd?: string; pid?: number }): vscode.MarkdownString {
48-
const tooltip = new vscode.MarkdownString(undefined, true);
49-
tooltip.appendMarkdown(`**${opts.label}**\n\n`);
50-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.workspace', 'Workspace')}: ${opts.scopeLabel}\n`);
51-
if (opts.pid !== undefined) {
52-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.pid', 'PID')}: ${opts.pid}\n`);
53-
}
54-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.port', 'Port')}: ${opts.portNumber}\n`);
55-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.started', 'Started')}: ${opts.startTime.toLocaleString()}\n`);
56-
if (opts.stopTime) {
57-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.stopped', 'Stopped')}: ${opts.stopTime.toLocaleString()}\n`);
58-
}
59-
if (opts.cwd) {
60-
tooltip.appendMarkdown(`- ${localize('funcHostDebug.cwd', 'CWD')}: ${opts.cwd}\n`);
61-
}
62-
return tooltip;
63-
}
64-
65-
function getNoHostTreeItem(): vscode.TreeItem {
66-
const item = new vscode.TreeItem(localize('funcHostDebug.noneRunning', 'No Function Host task is currently running.'), vscode.TreeItemCollapsibleState.None);
67-
item.description = localize('funcHostDebug.startDebuggingHint', 'Start debugging (F5) to launch the host.');
68-
item.iconPath = new vscode.ThemeIcon('debug');
69-
return item;
70-
}
71-
72-
function getHostErrorTreeItem(element: IHostErrorNode): vscode.TreeItem {
73-
const firstLine = element.message.split(/\r?\n/)[0].trim();
74-
const label = firstLine || localize('funcHostDebug.errorDetected', 'Error detected');
75-
76-
const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.None);
77-
item.iconPath = new vscode.ThemeIcon('error');
78-
item.tooltip = element.message;
79-
item.contextValue = FuncHostDebugContextValue.HostError;
80-
return item;
81-
}
82-
83-
function getHostTaskTreeItem(element: IHostTaskNode): vscode.TreeItem {
84-
const task = runningFuncTaskMap.get(element.workspaceFolder, element.cwd);
85-
const scopeLabel = typeof element.workspaceFolder === 'object'
86-
? element.workspaceFolder.name
87-
: localize('funcHostDebug.globalScope', 'Global');
88-
89-
const label = `${scopeLabel} (${element.portNumber})`;
90-
91-
const tooltip = buildHostTooltip({ label, scopeLabel, portNumber: element.portNumber, startTime: element.startTime, cwd: element.cwd, pid: task?.processId });
92-
93-
const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.Expanded);
94-
item.description = formatTimestamp(element.startTime);
95-
item.tooltip = tooltip;
96-
item.contextValue = FuncHostDebugContextValue.HostTask;
97-
item.iconPath = new vscode.ThemeIcon('server-process');
98-
return item;
99-
}
100-
101-
function getStoppedHostTreeItem(element: IStoppedHostNode): vscode.TreeItem {
102-
const stopped = element.stoppedTask;
103-
const scopeLabel = typeof stopped.workspaceFolder === 'object'
104-
? stopped.workspaceFolder.name
105-
: localize('funcHostDebug.globalScope', 'Global');
106-
107-
const label = `${scopeLabel} (${stopped.portNumber}) — Stopped`;
108-
109-
const tooltip = buildHostTooltip({ label, scopeLabel, portNumber: stopped.portNumber, startTime: stopped.startTime, stopTime: stopped.stopTime, cwd: stopped.cwd });
110-
111-
const errorCount = stopped.errorLogs.length;
112-
const item = new vscode.TreeItem(label, errorCount > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None);
113-
item.description = `${formatTimestamp(stopped.startTime)}${formatTimestamp(stopped.stopTime)}`;
114-
item.tooltip = tooltip;
115-
item.contextValue = FuncHostDebugContextValue.StoppedHostTask;
116-
item.iconPath = new vscode.ThemeIcon('debug-stop', new vscode.ThemeColor('disabledForeground'));
117-
return item;
118-
}
18+
export type FuncHostDebugNode = NoHostNode | HostTaskNode | StoppedHostNode | HostErrorNode;
11919

12020
export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHostDebugNode> {
12121
private readonly _onDidChangeTreeDataEmitter = new vscode.EventEmitter<FuncHostDebugNode | undefined>();
@@ -126,50 +26,12 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
12626
}
12727

12828
public getTreeItem(element: FuncHostDebugNode): vscode.TreeItem {
129-
switch (element.kind) {
130-
case 'noHost':
131-
return getNoHostTreeItem();
132-
case 'hostError':
133-
return getHostErrorTreeItem(element);
134-
case 'hostTask':
135-
return getHostTaskTreeItem(element);
136-
case 'stoppedHost':
137-
return getStoppedHostTreeItem(element);
138-
default: {
139-
// Exhaustive check: if we reach here, the FuncHostDebugNode union is out of sync with this switch.
140-
throw new Error(`Unexpected FuncHostDebugNode kind: ${(element as { kind?: unknown }).kind}`);
141-
}
142-
}
29+
return element.getTreeItem();
14330
}
14431

14532
public async getChildren(element?: FuncHostDebugNode): Promise<FuncHostDebugNode[]> {
146-
if (element?.kind === 'hostTask') {
147-
const task = runningFuncTaskMap.get(element.workspaceFolder, element.cwd);
148-
const errors = task?.errorLogs ?? [];
149-
return errors
150-
.slice()
151-
.reverse()
152-
.map((message): IHostErrorNode => ({
153-
kind: 'hostError',
154-
workspaceFolder: element.workspaceFolder,
155-
cwd: element.cwd,
156-
portNumber: element.portNumber,
157-
message,
158-
}));
159-
} else if (element?.kind === 'stoppedHost') {
160-
const stopped = element.stoppedTask;
161-
return stopped.errorLogs
162-
.slice()
163-
.reverse()
164-
.map((message): IHostErrorNode => ({
165-
kind: 'hostError',
166-
workspaceFolder: stopped.workspaceFolder,
167-
cwd: stopped.cwd,
168-
portNumber: stopped.portNumber,
169-
message,
170-
}));
171-
} else if (element) {
172-
return [];
33+
if (element) {
34+
return element.getChildren();
17335
}
17436

17537
const nodes: FuncHostDebugNode[] = [];
@@ -182,19 +44,19 @@ export class FuncHostDebugViewProvider implements vscode.TreeDataProvider<FuncHo
18244
continue;
18345
}
18446
const cwd = (t.taskExecution.task.execution as vscode.ShellExecution | undefined)?.options?.cwd;
185-
nodes.push({ kind: 'hostTask', workspaceFolder: folder, cwd, portNumber: t.portNumber, startTime: t.startTime });
47+
nodes.push(new HostTaskNode(folder, t.portNumber, t.startTime, cwd));
18648
hasRunning = true;
18749
}
18850
}
18951

19052
// Always show the hint node when no host is actively running.
19153
if (!hasRunning) {
192-
nodes.push({ kind: 'noHost' });
54+
nodes.push(new NoHostNode());
19355
}
19456

19557
// Stopped sessions (already newest-first in the array).
19658
for (const stopped of stoppedFuncTasks) {
197-
nodes.push({ kind: 'stoppedHost', stoppedTask: stopped });
59+
nodes.push(new StoppedHostNode(stopped));
19860
}
19961

20062
return nodes;

src/debug/nodes/HostErrorNode.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { localize } from '../../localize';
8+
9+
enum FuncHostDebugContextValue {
10+
HostError = 'azFunc.funcHostDebug.hostError',
11+
}
12+
13+
export class HostErrorNode {
14+
public readonly kind = 'hostError' as const;
15+
16+
constructor(
17+
public readonly workspaceFolder: vscode.WorkspaceFolder | vscode.TaskScope,
18+
public readonly portNumber: string,
19+
public readonly message: string,
20+
public readonly cwd?: string,
21+
) { }
22+
23+
public getTreeItem(): vscode.TreeItem {
24+
const firstLine = this.message.split(/\r?\n/)[0].trim();
25+
const label = firstLine || localize('funcHostDebug.errorDetected', 'Error detected');
26+
27+
const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.None);
28+
item.iconPath = new vscode.ThemeIcon('error');
29+
item.tooltip = this.message;
30+
item.contextValue = FuncHostDebugContextValue.HostError;
31+
return item;
32+
}
33+
34+
public getChildren(): never[] {
35+
return [];
36+
}
37+
}

src/debug/nodes/HostTaskNode.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { runningFuncTaskMap } from '../../funcCoreTools/funcHostTask';
8+
import { buildHostTooltip, formatTimestamp, getScopeLabel } from './funcHostDebugUtils';
9+
import { HostErrorNode } from './HostErrorNode';
10+
11+
enum FuncHostDebugContextValue {
12+
HostTask = 'azFunc.funcHostDebug.hostTask',
13+
}
14+
15+
export class HostTaskNode {
16+
public readonly kind = 'hostTask' as const;
17+
18+
constructor(
19+
public readonly workspaceFolder: vscode.WorkspaceFolder | vscode.TaskScope,
20+
public readonly portNumber: string,
21+
public readonly startTime: Date,
22+
public readonly cwd?: string,
23+
) { }
24+
25+
public getTreeItem(): vscode.TreeItem {
26+
const task = runningFuncTaskMap.get(this.workspaceFolder, this.cwd);
27+
const scopeLabel = getScopeLabel(this.workspaceFolder);
28+
const label = `${scopeLabel} (${this.portNumber})`;
29+
30+
const tooltip = buildHostTooltip({ label, scopeLabel, portNumber: this.portNumber, startTime: this.startTime, cwd: this.cwd, pid: task?.processId });
31+
32+
const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.Expanded);
33+
item.description = formatTimestamp(this.startTime);
34+
item.tooltip = tooltip;
35+
item.contextValue = FuncHostDebugContextValue.HostTask;
36+
item.iconPath = new vscode.ThemeIcon('server-process');
37+
return item;
38+
}
39+
40+
public getChildren(): HostErrorNode[] {
41+
const task = runningFuncTaskMap.get(this.workspaceFolder, this.cwd);
42+
const errors = task?.errorLogs ?? [];
43+
return errors
44+
.slice()
45+
.reverse()
46+
.map((message) => new HostErrorNode(this.workspaceFolder, this.portNumber, message, this.cwd));
47+
}
48+
}

src/debug/nodes/NoHostNode.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { localize } from '../../localize';
8+
9+
export class NoHostNode {
10+
public readonly kind = 'noHost' as const;
11+
12+
public getTreeItem(): vscode.TreeItem {
13+
const item = new vscode.TreeItem(localize('funcHostDebug.noneRunning', 'No Function Host task is currently running.'), vscode.TreeItemCollapsibleState.None);
14+
item.description = localize('funcHostDebug.startDebuggingHint', 'Start debugging (F5) to launch the host.');
15+
item.iconPath = new vscode.ThemeIcon('debug');
16+
return item;
17+
}
18+
19+
public getChildren(): never[] {
20+
return [];
21+
}
22+
}

src/debug/nodes/StoppedHostNode.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { type IStoppedFuncTask } from '../../funcCoreTools/funcHostTask';
8+
import { buildHostTooltip, formatTimestamp, getScopeLabel } from './funcHostDebugUtils';
9+
import { HostErrorNode } from './HostErrorNode';
10+
11+
enum FuncHostDebugContextValue {
12+
StoppedHostTask = 'azFunc.funcHostDebug.stoppedHostTask',
13+
}
14+
15+
export class StoppedHostNode {
16+
public readonly kind = 'stoppedHost' as const;
17+
18+
constructor(public readonly stoppedTask: IStoppedFuncTask) { }
19+
20+
public getTreeItem(): vscode.TreeItem {
21+
const stopped = this.stoppedTask;
22+
const scopeLabel = getScopeLabel(stopped.workspaceFolder);
23+
const label = `${scopeLabel} (${stopped.portNumber}) — Stopped`;
24+
25+
const tooltip = buildHostTooltip({ label, scopeLabel, portNumber: stopped.portNumber, startTime: stopped.startTime, stopTime: stopped.stopTime, cwd: stopped.cwd });
26+
27+
const errorCount = stopped.errorLogs.length;
28+
const item = new vscode.TreeItem(label, errorCount > 0 ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.None);
29+
item.description = `${formatTimestamp(stopped.startTime)}${formatTimestamp(stopped.stopTime)}`;
30+
item.tooltip = tooltip;
31+
item.contextValue = FuncHostDebugContextValue.StoppedHostTask;
32+
item.iconPath = new vscode.ThemeIcon('debug-stop', new vscode.ThemeColor('disabledForeground'));
33+
return item;
34+
}
35+
36+
public getChildren(): HostErrorNode[] {
37+
const stopped = this.stoppedTask;
38+
return stopped.errorLogs
39+
.slice()
40+
.reverse()
41+
.map((message) => new HostErrorNode(stopped.workspaceFolder, stopped.portNumber, message, stopped.cwd));
42+
}
43+
}

0 commit comments

Comments
 (0)