Skip to content

Commit fc7784c

Browse files
authored
fix(vscode): support no file selected (#36)
Fixes #16 <img width="593" height="618" alt="image" src="https://github.com/user-attachments/assets/c4bdc6eb-41cf-45f5-84a9-3fa9c2588b45" /> Signed-off-by: karan-palan <karanpalan007@gmail.com>
1 parent 4d413c5 commit fc7784c

11 files changed

Lines changed: 158 additions & 23 deletions

File tree

shared/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export interface PanelState {
7272
formatLoading?: boolean;
7373
hasParseErrors?: boolean;
7474
blockedByMetaschema?: boolean;
75+
noFileSelected?: boolean;
7576
}
7677

7778
export interface WebviewMessage {

test/vscode/extension.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,41 @@ suite('Extension Test Suite', () => {
8787
assert.ok(extension?.packageJSON.version, 'Extension should have a version in package.json');
8888
assert.match(extension?.packageJSON.version, /^\d+\.\d+\.\d+$/, 'Version should follow semver format');
8989
});
90+
91+
test('Should handle no file selected gracefully', async function() {
92+
this.timeout(5000);
93+
94+
const extension = vscode.extensions.getExtension('sourcemeta.studio');
95+
if (extension && !extension.isActive) {
96+
await extension.activate();
97+
}
98+
99+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
100+
101+
await new Promise(resolve => setTimeout(resolve, 500));
102+
103+
await vscode.commands.executeCommand('sourcemeta-studio.openPanel');
104+
105+
await new Promise(resolve => setTimeout(resolve, 1000));
106+
107+
assert.ok(true, 'Extension should handle no file selected without errors');
108+
});
109+
110+
test('Should show appropriate message when no file is selected', async function() {
111+
this.timeout(5000);
112+
113+
const extension = vscode.extensions.getExtension('sourcemeta.studio');
114+
if (extension && !extension.isActive) {
115+
await extension.activate();
116+
}
117+
118+
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
119+
await new Promise(resolve => setTimeout(resolve, 500));
120+
121+
await vscode.commands.executeCommand('sourcemeta-studio.openPanel');
122+
await new Promise(resolve => setTimeout(resolve, 1000));
123+
124+
assert.ok(extension, 'Extension should exist');
125+
assert.ok(extension?.isActive, 'Extension should remain active with no file selected');
126+
});
90127
});

test/vscode/panelManager.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ suite('PanelManager Test Suite', () => {
4545
);
4646
});
4747

48+
test('should handle no file selected state', () => {
49+
const noFileState: PanelState = {
50+
fileInfo: null,
51+
cliVersion: '12.2.0',
52+
extensionVersion: '0.0.1',
53+
lintResult: { raw: '', health: null },
54+
formatResult: { output: '', exitCode: null },
55+
metaschemaResult: { output: '', exitCode: null },
56+
isLoading: false,
57+
noFileSelected: true
58+
};
59+
60+
assert.doesNotThrow(
61+
() => panelManager.updateContent(noFileState),
62+
'Should handle no file selected state'
63+
);
64+
});
65+
4866
test('should have setMessageHandler method', () => {
4967
assert.ok(
5068
typeof panelManager.setMessageHandler === 'function',

vscode/src/extension.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,24 @@ async function updatePanelContent(): Promise<void> {
215215
const filePath = lastActiveTextEditor?.document.uri.fsPath;
216216
const fileInfo = getFileInfo(filePath);
217217

218+
// If no file is selected, show a "no file" state without running commands
219+
if (!fileInfo) {
220+
const noFileState: PanelState = {
221+
fileInfo: null,
222+
cliVersion: cachedCliVersion || 'Unknown',
223+
extensionVersion,
224+
lintResult: { raw: '', health: null },
225+
formatResult: { output: '', exitCode: null },
226+
metaschemaResult: { output: '', exitCode: null },
227+
isLoading: false,
228+
hasParseErrors: false,
229+
noFileSelected: true
230+
};
231+
currentPanelState = noFileState;
232+
panelManager.updateContent(noFileState);
233+
return;
234+
}
235+
218236
// Send initial loading state
219237
const loadingState: PanelState = {
220238
fileInfo,
@@ -237,7 +255,7 @@ async function updatePanelContent(): Promise<void> {
237255
const version = await commandExecutor.getVersion();
238256
cachedCliVersion = version;
239257

240-
const metaschemaRawResult = fileInfo ? await commandExecutor.metaschema(fileInfo.absolutePath) : { output: 'No file selected', exitCode: null };
258+
const metaschemaRawResult = await commandExecutor.metaschema(fileInfo.absolutePath);
241259
const metaschemaResult = parseMetaschemaResult(metaschemaRawResult.output, metaschemaRawResult.exitCode);
242260

243261
if (metaschemaResult.errors && metaschemaResult.errors.length > 0) {
@@ -266,8 +284,8 @@ async function updatePanelContent(): Promise<void> {
266284
}
267285

268286
const [lintOutput, formatResult] = await Promise.all([
269-
fileInfo ? commandExecutor.lint(fileInfo.absolutePath) : Promise.resolve('No file selected'),
270-
fileInfo ? commandExecutor.formatCheck(fileInfo.absolutePath) : Promise.resolve({ output: 'No file selected', exitCode: null })
287+
commandExecutor.lint(fileInfo.absolutePath),
288+
commandExecutor.formatCheck(fileInfo.absolutePath)
271289
]);
272290

273291
const lintResult = parseLintResult(lintOutput);

webview/src/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function App() {
6363
lintResult={state.lintResult}
6464
isLoading={state.isLoading}
6565
blockedByMetaschema={state.blockedByMetaschema}
66+
noFileSelected={state.noFileSelected}
6667
/>
6768
<Tabs activeTab={activeTab} onTabChange={handleTabChange} state={state} />
6869

@@ -73,9 +74,9 @@ function App() {
7374
<LoadingSpinner fileInfo={state.fileInfo} />
7475
) : (
7576
<>
76-
{activeTab === 'lint' && <LintTab lintResult={state.lintResult} blocked={!!state.blockedByMetaschema} />}
77-
{activeTab === 'format' && <FormatTab formatResult={state.formatResult} fileInfo={state.fileInfo} hasParseErrors={state.hasParseErrors} blocked={!!state.blockedByMetaschema} />}
78-
{activeTab === 'metaschema' && <MetaschemaTab metaschemaResult={state.metaschemaResult} />}
77+
{activeTab === 'lint' && <LintTab lintResult={state.lintResult} blocked={!!state.blockedByMetaschema} noFileSelected={state.noFileSelected} />}
78+
{activeTab === 'format' && <FormatTab formatResult={state.formatResult} fileInfo={state.fileInfo} hasParseErrors={state.hasParseErrors} blocked={!!state.blockedByMetaschema} noFileSelected={state.noFileSelected} />}
79+
{activeTab === 'metaschema' && <MetaschemaTab metaschemaResult={state.metaschemaResult} noFileSelected={state.noFileSelected} />}
7980
</>
8081
)}
8182
</div>

webview/src/components/FormatTab.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,37 @@
11
import type { CommandResult, FileInfo } from '../../../shared/types.ts';
22
import { vscode } from '../vscode-api';
33
import { RawOutput } from './RawOutput';
4-
import { Info, CheckCircle, AlertCircle } from 'lucide-react';
4+
import { Info, CheckCircle, AlertCircle, FileQuestion } from 'lucide-react';
55

66
export interface FormatTabProps {
77
formatResult: CommandResult;
88
fileInfo: FileInfo | null;
99
hasParseErrors?: boolean;
1010
blocked?: boolean;
11+
noFileSelected?: boolean;
1112
}
1213

13-
export function FormatTab({ formatResult, fileInfo, hasParseErrors, blocked }: FormatTabProps) {
14+
export function FormatTab({ formatResult, fileInfo, hasParseErrors, blocked, noFileSelected }: FormatTabProps) {
1415
const handleFormatSchema = () => {
1516
vscode.postMessage({ command: 'formatSchema' });
1617
};
1718

1819
const isYaml = fileInfo?.isYaml || false;
1920

21+
if (noFileSelected) {
22+
return (
23+
<div className="text-center py-10 px-5">
24+
<div className="flex justify-center mb-4">
25+
<FileQuestion size={48} className="text-(--vscode-muted)" strokeWidth={1.5} />
26+
</div>
27+
<div className="text-lg font-semibold text-(--vscode-fg) mb-2">No Schema File Selected</div>
28+
<div className="text-[13px] text-(--vscode-muted) max-w-md mx-auto">
29+
Open a JSON schema file to check formatting.
30+
</div>
31+
</div>
32+
);
33+
}
34+
2035
if (hasParseErrors) {
2136
return (
2237
<div className="text-center py-10 px-5">

webview/src/components/HealthBar.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ export interface HealthBarProps {
44
lintResult: LintResult;
55
isLoading?: boolean;
66
blockedByMetaschema?: boolean;
7+
noFileSelected?: boolean;
78
}
89

9-
export function HealthBar({ lintResult, isLoading, blockedByMetaschema }: HealthBarProps) {
10+
export function HealthBar({ lintResult, isLoading, blockedByMetaschema, noFileSelected }: HealthBarProps) {
1011
const errorCount = lintResult.errors?.length || 0;
1112

1213
let health: number;
1314

14-
if (blockedByMetaschema) {
15+
if (noFileSelected || blockedByMetaschema) {
1516
health = 0;
1617
} else if (lintResult.health !== null && lintResult.health !== undefined) {
1718
health = lintResult.health;
@@ -27,12 +28,14 @@ export function HealthBar({ lintResult, isLoading, blockedByMetaschema }: Health
2728
return 'var(--error)';
2829
};
2930

30-
const showUnknown = !blockedByMetaschema && (isLoading || (lintResult.health === null && lintResult.errors === undefined));
31+
const showUnknown = !blockedByMetaschema && !noFileSelected && (isLoading || (lintResult.health === null && lintResult.errors === undefined));
3132

3233
return (
3334
<div className="mb-5">
3435
<div className="text-(--vscode-fg) text-xs mb-1.5 font-semibold">
35-
Schema Health: {blockedByMetaschema ? (
36+
Schema Health: {noFileSelected ? (
37+
<span className="text-(--vscode-muted)">N/A</span>
38+
) : blockedByMetaschema ? (
3639
<span className="text-(--vscode-muted)">N/A</span>
3740
) : showUnknown ? (
3841
<span className="text-(--vscode-muted)">?%</span>
@@ -41,7 +44,7 @@ export function HealthBar({ lintResult, isLoading, blockedByMetaschema }: Health
4144
)}
4245
</div>
4346
<div className="w-full h-2 bg-(--vscode-selection) rounded overflow-hidden">
44-
{showUnknown ? (
47+
{showUnknown || noFileSelected ? (
4548
<div className="h-full bg-(--vscode-muted) opacity-30 w-full" />
4649
) : (
4750
<div

webview/src/components/LintTab.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
11
import type { LintResult } from '../../../shared/types.ts';
22
import { vscode } from '../vscode-api';
33
import { RawOutput } from './RawOutput';
4-
import { CheckCircle, AlertCircle } from 'lucide-react';
4+
import { CheckCircle, AlertCircle, FileQuestion } from 'lucide-react';
55

66
export interface LintTabProps {
77
lintResult: LintResult;
88
blocked?: boolean;
9+
noFileSelected?: boolean;
910
}
1011

11-
export function LintTab({ lintResult, blocked }: LintTabProps) {
12+
export function LintTab({ lintResult, blocked, noFileSelected }: LintTabProps) {
1213
const handleGoToPosition = (position: [number, number, number, number]) => {
1314
vscode.postMessage({ command: 'goToPosition', position });
1415
};
1516

1617
const errors = lintResult.errors || [];
1718

19+
if (noFileSelected) {
20+
return (
21+
<div className="text-center py-10 px-5">
22+
<div className="flex justify-center mb-4">
23+
<FileQuestion size={48} className="text-(--vscode-muted)" strokeWidth={1.5} />
24+
</div>
25+
<div className="text-lg font-semibold text-(--vscode-fg) mb-2">No Schema File Selected</div>
26+
<div className="text-[13px] text-(--vscode-muted) max-w-md mx-auto">
27+
Open a JSON or YAML schema file to see linting results.
28+
</div>
29+
</div>
30+
);
31+
}
32+
1833
if (blocked) {
1934
return (
2035
<div className="text-center py-10 px-5">

webview/src/components/MetaschemaTab.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
import type { MetaschemaResult, MetaschemaError } from '../../../shared/types.ts';
22
import { vscode } from '../vscode-api';
33
import { RawOutput } from './RawOutput';
4-
import { CheckCircle, AlertTriangle } from 'lucide-react';
4+
import { CheckCircle, AlertTriangle, FileQuestion } from 'lucide-react';
55

66
export interface MetaschemaTabProps {
77
metaschemaResult: MetaschemaResult;
8+
noFileSelected?: boolean;
89
}
910

1011
function isMetaschemaError(error: unknown): error is MetaschemaError {
1112
return typeof error === 'object' && error !== null && 'instancePosition' in error;
1213
}
1314

14-
export function MetaschemaTab({ metaschemaResult }: MetaschemaTabProps) {
15+
export function MetaschemaTab({ metaschemaResult, noFileSelected }: MetaschemaTabProps) {
1516
const handleGoToPosition = (position: [number, number, number, number]) => {
1617
vscode.postMessage({ command: 'goToPosition', position });
1718
};
1819

1920
const errors = metaschemaResult.errors || [];
2021
const metaschemaErrors = errors.filter(isMetaschemaError);
2122

23+
if (noFileSelected) {
24+
return (
25+
<div className="text-center py-10 px-5">
26+
<div className="flex justify-center mb-4">
27+
<FileQuestion size={48} className="text-(--vscode-muted)" strokeWidth={1.5} />
28+
</div>
29+
<div className="text-lg font-semibold text-(--vscode-fg) mb-2">No Schema File Selected</div>
30+
<div className="text-[13px] text-(--vscode-muted) max-w-md mx-auto">
31+
Open a JSON or YAML schema file to validate against its meta-schema.
32+
</div>
33+
</div>
34+
);
35+
}
36+
2237
if (metaschemaResult.exitCode === 0) {
2338
return (
2439
<>

webview/src/components/Tabs.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ interface TabProps {
1717
}
1818

1919
export function Tabs({ activeTab, onTabChange, state }: TabsProps) {
20-
const lintStatus = calculateLintStatus(state.lintResult.errors?.length || 0, state.lintResult.health, state.isLoading);
21-
const formatStatus = calculateFormatStatus(state.formatResult.exitCode, state.formatLoading, state.fileInfo?.isYaml);
22-
const metaschemaStatus = calculateMetaschemaStatus(state.metaschemaResult.exitCode, state.isLoading);
20+
const lintStatus = calculateLintStatus(state.lintResult.errors?.length || 0, state.lintResult.health, state.isLoading, state.noFileSelected);
21+
const formatStatus = calculateFormatStatus(state.formatResult.exitCode, state.formatLoading, state.fileInfo?.isYaml, state.noFileSelected);
22+
const metaschemaStatus = calculateMetaschemaStatus(state.metaschemaResult.exitCode, state.isLoading, state.noFileSelected);
2323
const lintDisabled = !!state.blockedByMetaschema;
2424

2525
const Tab = ({

0 commit comments

Comments
 (0)