Skip to content

Commit e36a36f

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 only check the folder of the Camel route. - findKaotoMetadataToCreate used path.basename instead of path.dirname when there is no workspace, so the file was created at a wrong location instead of next to the Camel route. - Add tests for both lookups, including a .kaoto file above the workspace root that must be ignored.
1 parent 515689a commit e36a36f

2 files changed

Lines changed: 165 additions & 12 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
if (workspaceKaotoBackup) {
67+
fs.renameSync(workspaceKaotoBackup, workspaceKaoto);
68+
}
69+
fs.rmSync(outsideRoot, { recursive: true, force: true });
70+
});
71+
72+
suite('findExistingKaotoMetadataFile', () => {
73+
test('finds the .kaoto file at the workspace root', async () => {
74+
fs.writeFileSync(workspaceKaoto, '{}');
75+
const route = vscode.Uri.file(path.join(workspaceFolder, 'routes', 'a.camel.yaml'));
76+
77+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
78+
79+
expect(found?.fsPath).to.equal(vscode.Uri.file(workspaceKaoto).fsPath);
80+
});
81+
82+
test('does not look above the workspace root, e.g. into the home directory', async () => {
83+
fs.writeFileSync(parentKaoto, '{"leaked":"from-another-project"}');
84+
const route = vscode.Uri.file(path.join(workspaceFolder, 'routes', 'a.camel.yaml'));
85+
86+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
87+
88+
expect(found).to.equal(undefined);
89+
});
90+
91+
test('prefers the workspace .kaoto over one located above the workspace root', async () => {
92+
fs.writeFileSync(parentKaoto, '{}');
93+
fs.writeFileSync(workspaceKaoto, '{}');
94+
const route = vscode.Uri.file(path.join(workspaceFolder, 'a.camel.yaml'));
95+
96+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
97+
98+
expect(found?.fsPath).to.equal(vscode.Uri.file(workspaceKaoto).fsPath);
99+
});
100+
101+
test('outside of any workspace, does not walk up the directory tree', async () => {
102+
fs.writeFileSync(path.join(outsideRoot, '.kaoto'), '{}');
103+
const route = vscode.Uri.file(path.join(outsideRoot, 'nested', 'a.camel.yaml'));
104+
105+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
106+
107+
expect(found).to.equal(undefined);
108+
});
109+
110+
test('outside of any workspace, finds the .kaoto next to the route', async () => {
111+
const nested = path.join(outsideRoot, 'nested');
112+
fs.mkdirSync(nested);
113+
fs.writeFileSync(path.join(nested, '.kaoto'), '{}');
114+
const route = vscode.Uri.file(path.join(nested, 'a.camel.yaml'));
115+
116+
const found = await createApi(route).findExistingKaotoMetadataFile(route);
117+
118+
expect(found?.fsPath).to.equal(vscode.Uri.file(path.join(nested, '.kaoto')).fsPath);
119+
});
120+
});
121+
122+
suite('findKaotoMetadataToCreate', () => {
123+
test('creates the .kaoto file at the workspace root', async () => {
124+
const route = vscode.Uri.file(path.join(workspaceFolder, 'routes', 'a.camel.yaml'));
125+
126+
const toCreate = await createApi(route).findKaotoMetadataToCreate();
127+
128+
expect(toCreate.fsPath).to.equal(vscode.Uri.file(workspaceKaoto).fsPath);
129+
});
130+
131+
test('outside of any workspace, creates the .kaoto file next to the route', async () => {
132+
const nested = path.join(outsideRoot, 'nested');
133+
const route = vscode.Uri.file(path.join(nested, 'a.camel.yaml'));
134+
135+
const toCreate = await createApi(route).findKaotoMetadataToCreate();
136+
137+
expect(toCreate.fsPath).to.equal(vscode.Uri.file(path.join(nested, '.kaoto')).fsPath);
138+
});
139+
});
140+
});

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -319,26 +319,39 @@ 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, only the folder containing the file is checked.
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) {
325-
return undefined;
326-
}
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));
328+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileUri)?.uri.toString();
329+
let folder = path.dirname(fileUri.fsPath);
330+
while (true) {
331+
const kaotoMetadataFileCandidate = vscode.Uri.file(path.join(folder, '.kaoto'));
332+
try {
333+
await vscode.workspace.fs.stat(kaotoMetadataFileCandidate);
334+
return kaotoMetadataFileCandidate;
335+
} catch {
336+
// not in this folder, try the parent one if allowed
337+
}
338+
const parentFolder = path.dirname(folder);
339+
// Let VS Code decide containment: comparing paths breaks on case-insensitive file systems and on Windows drive letters.
340+
const isParentInWorkspaceFolder =
341+
workspaceFolder !== undefined && vscode.workspace.getWorkspaceFolder(vscode.Uri.file(parentFolder))?.uri.toString() === workspaceFolder;
342+
if (parentFolder === folder || !isParentInWorkspaceFolder) {
343+
return undefined;
344+
}
345+
folder = parentFolder;
333346
}
334347
}
335348

336349
private async findKaotoMetadataToCreate(): Promise<vscode.Uri> {
337350
const workspaceFolder = vscode.workspace.getWorkspaceFolder(this.currentEditedDocument.uri);
338351
if (workspaceFolder !== undefined) {
339-
return vscode.Uri.file(path.join(workspaceFolder?.uri.fsPath, '.kaoto'));
352+
return vscode.Uri.file(path.join(workspaceFolder.uri.fsPath, '.kaoto'));
340353
} else {
341-
const parentFolder = path.basename(path.dirname(this.currentEditedDocument.uri.fsPath));
354+
const parentFolder = path.dirname(this.currentEditedDocument.uri.fsPath);
342355
return vscode.Uri.file(path.join(parentFolder, '.kaoto'));
343356
}
344357
}

0 commit comments

Comments
 (0)