Skip to content

Commit 1079fb2

Browse files
committed
fix(vscode): stop the .kaoto metadata lookup at the workspace root
- findExistingKaotoMetadataFile walked up to the filesystem root, so a .kaoto file in the home directory was found for every project below it and no new file was created in the workspace. Stop at the workspace folder, letting vscode.workspace.getWorkspaceFolder() decide the boundary. - Outside a workspace folder nothing is looked up and findKaotoMetadataToCreate fails instead of creating a .kaoto file at a wrong location (it used path.basename instead of path.dirname). - Add tests for both lookups, including a .kaoto file above the workspace root that must be ignored.
1 parent 515689a commit 1079fb2

2 files changed

Lines changed: 179 additions & 15 deletions

File tree

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
import { expect } from 'chai';
18+
import * as fs from 'node:fs';
19+
import * as os from 'node:os';
20+
import * as path from 'node:path';
21+
import * as vscode from 'vscode';
22+
import { VSCodeKaotoEditorChannelApi } from '../../webview/VSCodeKaotoEditorChannelApi';
23+
import { getTestWorkspaceFolder } from '../utils/TestSetup';
24+
25+
/**
26+
* The lookup of the `.kaoto` metadata file only depends on the edited document URI,
27+
* so the private methods are invoked on a minimal object instead of a full editor channel.
28+
*/
29+
interface MetadataFileLookup {
30+
findExistingKaotoMetadataFile(fileUri: vscode.Uri): Promise<vscode.Uri | undefined>;
31+
findKaotoMetadataToCreate(): Promise<vscode.Uri>;
32+
}
33+
34+
function createApi(documentUri: vscode.Uri): MetadataFileLookup {
35+
const api = Object.create(VSCodeKaotoEditorChannelApi.prototype);
36+
api.currentEditedDocument = { uri: documentUri };
37+
return api as MetadataFileLookup;
38+
}
39+
40+
suite('VSCodeKaotoEditorChannelApi .kaoto metadata file lookup', () => {
41+
let workspaceFolder: string;
42+
let workspaceKaoto: string;
43+
let workspaceKaotoBackup: string | undefined;
44+
let outsideRoot: string;
45+
let parentKaoto: string;
46+
47+
setup(() => {
48+
workspaceFolder = getTestWorkspaceFolder();
49+
workspaceKaoto = path.join(workspaceFolder, '.kaoto');
50+
// The parent of the workspace folder plays the role of the user home directory
51+
parentKaoto = path.join(path.dirname(workspaceFolder), '.kaoto');
52+
53+
// The fixture ships a .kaoto in the workspace root; move it aside so that it does not shadow the cases below
54+
workspaceKaotoBackup = undefined;
55+
if (fs.existsSync(workspaceKaoto)) {
56+
workspaceKaotoBackup = `${workspaceKaoto}.bak`;
57+
fs.renameSync(workspaceKaoto, workspaceKaotoBackup);
58+
}
59+
60+
outsideRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'kaoto-metadata-outside-'));
61+
});
62+
63+
teardown(() => {
64+
fs.rmSync(parentKaoto, { force: true });
65+
fs.rmSync(workspaceKaoto, { force: true });
66+
fs.rmSync(path.join(workspaceFolder, 'routes'), { recursive: true, force: true });
67+
if (workspaceKaotoBackup) {
68+
fs.renameSync(workspaceKaotoBackup, workspaceKaoto);
69+
}
70+
fs.rmSync(outsideRoot, { recursive: true, force: true });
71+
});
72+
73+
suite('findExistingKaotoMetadataFile', () => {
74+
test('finds the .kaoto file at the workspace root', async () => {
75+
fs.writeFileSync(workspaceKaoto, '{}');
76+
const route = vscode.Uri.file(path.join(workspaceFolder, 'routes', 'a.camel.yaml'));
77+
78+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
79+
80+
expect(found?.fsPath).to.equal(vscode.Uri.file(workspaceKaoto).fsPath);
81+
});
82+
83+
test('does not look above the workspace root, e.g. into the home directory', async () => {
84+
fs.writeFileSync(parentKaoto, '{"leaked":"from-another-project"}');
85+
const route = vscode.Uri.file(path.join(workspaceFolder, 'routes', 'a.camel.yaml'));
86+
87+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
88+
89+
expect(found).to.be.undefined;
90+
});
91+
92+
test('prefers the workspace .kaoto over one located above the workspace root', async () => {
93+
fs.writeFileSync(parentKaoto, '{}');
94+
fs.writeFileSync(workspaceKaoto, '{}');
95+
const route = vscode.Uri.file(path.join(workspaceFolder, 'a.camel.yaml'));
96+
97+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
98+
99+
expect(found?.fsPath).to.equal(vscode.Uri.file(workspaceKaoto).fsPath);
100+
});
101+
102+
test('finds the closest .kaoto file between the route and the workspace root', async () => {
103+
const nested = path.join(workspaceFolder, 'routes');
104+
fs.mkdirSync(nested, { recursive: true });
105+
fs.writeFileSync(workspaceKaoto, '{}');
106+
fs.writeFileSync(path.join(nested, '.kaoto'), '{}');
107+
const route = vscode.Uri.file(path.join(nested, 'a.camel.yaml'));
108+
109+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
110+
111+
expect(found?.fsPath).to.equal(vscode.Uri.file(path.join(nested, '.kaoto')).fsPath);
112+
});
113+
114+
test('outside of any workspace, does not look up any .kaoto file', async () => {
115+
fs.writeFileSync(path.join(outsideRoot, '.kaoto'), '{}');
116+
const route = vscode.Uri.file(path.join(outsideRoot, 'a.camel.yaml'));
117+
118+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
119+
120+
expect(found).to.be.undefined;
121+
});
122+
});
123+
124+
suite('findKaotoMetadataToCreate', () => {
125+
test('creates the .kaoto file at the workspace root', async () => {
126+
const route = vscode.Uri.file(path.join(workspaceFolder, 'routes', 'a.camel.yaml'));
127+
128+
const toCreate = await createApi(route).findKaotoMetadataToCreate();
129+
130+
expect(toCreate.fsPath).to.equal(vscode.Uri.file(workspaceKaoto).fsPath);
131+
});
132+
133+
test('outside of any workspace, fails instead of creating a .kaoto file', async () => {
134+
const route = vscode.Uri.file(path.join(outsideRoot, 'a.camel.yaml'));
135+
136+
let error: unknown;
137+
try {
138+
await createApi(route).findKaotoMetadataToCreate();
139+
} catch (ex) {
140+
error = ex;
141+
}
142+
143+
expect(error).to.be.instanceOf(Error);
144+
});
145+
});
146+
});

‎packages/kaoto-vscode/src/webview/VSCodeKaotoEditorChannelApi.ts‎

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class VSCodeKaotoEditorChannelApi extends DefaultVsCodeKieEditorChannelAp
156156

157157
/**
158158
* Store metadata in the nearest .kaoto file found.
159-
* If none found, create the file at workspace root if available, otherwise to the side of the Camel route
159+
* If none found, create the file at the workspace root. Fails when the Camel route is not in a workspace folder.
160160
*/
161161
async setMetadata<T>(key: string, value: T | undefined): Promise<void> {
162162
let kaotoMetadatafile: vscode.Uri | undefined = await this.findExistingKaotoMetadataFile(this.currentEditedDocument.uri);
@@ -319,28 +319,46 @@ export class VSCodeKaotoEditorChannelApi extends DefaultVsCodeKieEditorChannelAp
319319
}
320320
}
321321

322+
/**
323+
* Look for a .kaoto file from the folder of the given file up to the workspace folder (included).
324+
* The lookup never goes above the workspace folder, so a .kaoto file located elsewhere (e.g. in the user home directory) is ignored.
325+
* When the file is not in a workspace folder, nothing is looked up.
326+
*/
322327
private async findExistingKaotoMetadataFile(fileUri: vscode.Uri): Promise<vscode.Uri | undefined> {
323-
const parentFolder = path.dirname(fileUri.fsPath);
324-
if (parentFolder === fileUri.fsPath || parentFolder === '' || parentFolder === undefined) {
328+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileUri)?.uri.toString();
329+
if (workspaceFolder === undefined) {
325330
return undefined;
326331
}
327-
try {
328-
const kaotoMetadataFileCandidate = vscode.Uri.file(path.join(parentFolder, '.kaoto'));
329-
await vscode.workspace.fs.stat(kaotoMetadataFileCandidate);
330-
return kaotoMetadataFileCandidate;
331-
} catch {
332-
return this.findExistingKaotoMetadataFile(vscode.Uri.file(parentFolder));
333-
}
332+
// Let VS Code decide containment: comparing paths breaks on case-insensitive file systems and on Windows drive letters.
333+
const isInWorkspaceFolder = (folderPath: string) =>
334+
vscode.workspace.getWorkspaceFolder(vscode.Uri.file(folderPath))?.uri.toString() === workspaceFolder;
335+
336+
let folder = path.dirname(fileUri.fsPath);
337+
let previousFolder: string;
338+
do {
339+
const kaotoMetadataFileCandidate = vscode.Uri.file(path.join(folder, '.kaoto'));
340+
try {
341+
await vscode.workspace.fs.stat(kaotoMetadataFileCandidate);
342+
return kaotoMetadataFileCandidate;
343+
} catch (ex) {
344+
// A missing file is expected, try the parent folder if it is still in the workspace folder. Anything else is worth a log.
345+
if (!(ex instanceof vscode.FileSystemError && ex.code === 'FileNotFound')) {
346+
KaotoOutputChannel.logError(`Cannot check the existence of ${kaotoMetadataFileCandidate.fsPath}`, ex);
347+
}
348+
}
349+
previousFolder = folder;
350+
folder = path.dirname(folder);
351+
} while (folder !== previousFolder && isInWorkspaceFolder(folder));
352+
353+
return undefined;
334354
}
335355

336356
private async findKaotoMetadataToCreate(): Promise<vscode.Uri> {
337357
const workspaceFolder = vscode.workspace.getWorkspaceFolder(this.currentEditedDocument.uri);
338-
if (workspaceFolder !== undefined) {
339-
return vscode.Uri.file(path.join(workspaceFolder?.uri.fsPath, '.kaoto'));
340-
} else {
341-
const parentFolder = path.basename(path.dirname(this.currentEditedDocument.uri.fsPath));
342-
return vscode.Uri.file(path.join(parentFolder, '.kaoto'));
358+
if (workspaceFolder === undefined) {
359+
throw new Error(`Cannot store Kaoto metadata: ${this.currentEditedDocument.uri.fsPath} is not in a workspace folder.`);
343360
}
361+
return vscode.Uri.file(path.join(workspaceFolder.uri.fsPath, '.kaoto'));
344362
}
345363

346364
/**

0 commit comments

Comments
 (0)