Skip to content

Commit e37fcbf

Browse files
committed
Add ObjectQL file creation, validation, and navigation
Introduces commands for creating new ObjectQL files from templates and validating current files. Adds definition and completion providers for 'reference_to' fields, and implements an ObjectIndex service for indexing object definitions. Refactors extension activation to register new commands and providers, and adds supporting constants and dependencies.
1 parent 6d450d1 commit e37fcbf

File tree

9 files changed

+346
-155
lines changed

9 files changed

+346
-155
lines changed

packages/tools/vscode-objectql/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,13 +218,17 @@
218218
"publish": "vsce publish"
219219
},
220220
"devDependencies": {
221+
"@types/js-yaml": "^4.0.9",
221222
"@types/node": "^20.10.0",
222223
"@types/vscode": "^1.85.0",
223224
"@vscode/test-electron": "^2.3.0",
224-
"typescript": "^5.3.0",
225-
"@vscode/vsce": "^3.7.1"
225+
"@vscode/vsce": "^3.7.1",
226+
"typescript": "^5.3.0"
226227
},
227228
"extensionDependencies": [
228229
"redhat.vscode-yaml"
229-
]
230+
],
231+
"dependencies": {
232+
"js-yaml": "^4.1.1"
233+
}
230234
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import * as vscode from 'vscode';
2+
import * as path from 'path';
3+
import * as fs from 'fs';
4+
5+
export async function createNewFile(
6+
context: vscode.ExtensionContext,
7+
fileType: 'object' | 'validation' | 'permission' | 'app'
8+
) {
9+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
10+
11+
if (!workspaceFolder) {
12+
vscode.window.showErrorMessage('Please open a workspace folder first');
13+
return;
14+
}
15+
16+
// Prompt for filename
17+
const fileName = await vscode.window.showInputBox({
18+
prompt: `Enter ${fileType} name (without extension)`,
19+
placeHolder: `my_${fileType}`,
20+
validateInput: (value: string) => {
21+
if (!value) {
22+
return 'Name cannot be empty';
23+
}
24+
if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
25+
return 'Name must start with lowercase letter or underscore and contain only lowercase letters, numbers, and underscores';
26+
}
27+
return null;
28+
}
29+
});
30+
31+
if (!fileName) {
32+
return;
33+
}
34+
35+
// Determine file path based on "AI-Native" folder structure
36+
let folder = 'src';
37+
if (fileType === 'object') {
38+
folder = 'src/objects';
39+
} else if (fileType === 'app') {
40+
folder = 'src';
41+
}
42+
43+
const fullFileName = `${fileName}.${fileType}.yml`;
44+
const defaultPath = vscode.Uri.joinPath(workspaceFolder.uri, folder, fullFileName);
45+
46+
// Get template content
47+
const template = getTemplate(context, fileType, fileName);
48+
49+
try {
50+
// Check if file already exists
51+
try {
52+
await vscode.workspace.fs.stat(defaultPath);
53+
const overwrite = await vscode.window.showWarningMessage(
54+
`File ${fullFileName} already exists. Overwrite?`,
55+
'Yes', 'No'
56+
);
57+
if (overwrite !== 'Yes') {
58+
return;
59+
}
60+
} catch {
61+
// File does not exist, proceed
62+
}
63+
64+
// Write file
65+
const edit = new vscode.WorkspaceEdit();
66+
edit.createFile(defaultPath, { overwrite: true, ignoreIfExists: false });
67+
edit.insert(defaultPath, new vscode.Position(0, 0), template);
68+
69+
const success = await vscode.workspace.applyEdit(edit);
70+
71+
if (success) {
72+
// Open file
73+
const document = await vscode.workspace.openTextDocument(defaultPath);
74+
await vscode.window.showTextDocument(document);
75+
vscode.window.showInformationMessage(`Created ${fullFileName}`);
76+
} else {
77+
vscode.window.showErrorMessage(`Failed to create file: ${fullFileName}`);
78+
}
79+
80+
} catch (error) {
81+
vscode.window.showErrorMessage(`Failed to create file: ${error}`);
82+
}
83+
}
84+
85+
function getTemplate(context: vscode.ExtensionContext, fileType: string, name: string): string {
86+
try {
87+
let templatePath = path.join(context.extensionPath, 'src', 'templates', `${fileType}.template.yml`);
88+
89+
if (!fs.existsSync(templatePath)) {
90+
templatePath = path.join(context.extensionPath, 'out', 'templates', `${fileType}.template.yml`);
91+
}
92+
93+
if (fs.existsSync(templatePath)) {
94+
let content = fs.readFileSync(templatePath, 'utf8');
95+
content = content.replace(/{{name}}/g, name);
96+
content = content.replace(/{{label}}/g, capitalizeWords(name));
97+
return content;
98+
}
99+
100+
return getFallbackTemplate(fileType, name);
101+
} catch (e) {
102+
console.error('Error reading template:', e);
103+
return getFallbackTemplate(fileType, name);
104+
}
105+
}
106+
107+
function getFallbackTemplate(fileType: string, name: string): string {
108+
return `# ${capitalizeWords(name)} ${fileType}\nname: ${name}\n`;
109+
}
110+
111+
function capitalizeWords(str: string): string {
112+
return str
113+
.split('_')
114+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
115+
.join(' ');
116+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as vscode from 'vscode';
2+
import * as path from 'path';
3+
4+
export async function validateCurrentFile() {
5+
const editor = vscode.window.activeTextEditor;
6+
if (!editor) {
7+
vscode.window.showWarningMessage('No active editor');
8+
return;
9+
}
10+
11+
const document = editor.document;
12+
const fileName = path.basename(document.fileName);
13+
14+
// Check if it's an ObjectQL file
15+
const objectqlFilePattern = /\.(object|validation|permission|app)\.(yml|yaml)$/;
16+
if (!objectqlFilePattern.test(fileName)) {
17+
vscode.window.showWarningMessage('This is not an ObjectQL metadata file');
18+
return;
19+
}
20+
21+
// Trigger validation by saving
22+
await document.save();
23+
24+
vscode.window.showInformationMessage('Validation complete. Check Problems panel for issues.');
25+
}
Lines changed: 26 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import * as vscode from 'vscode';
2-
import * as path from 'path';
3-
import * as fs from 'fs';
2+
import { createNewFile } from './commands/createFile';
3+
import { validateCurrentFile } from './commands/validate';
4+
import { ObjectIndex } from './services/ObjectIndex';
5+
import { ObjectDefinitionProvider } from './providers/ObjectDefinitionProvider';
6+
import { ObjectCompletionProvider } from './providers/ObjectCompletionProvider';
7+
import { LANGUAGES, SCHEMES } from './utils/constants';
8+
9+
let objectIndex: ObjectIndex;
410

511
/**
612
* Extension activation function
@@ -9,7 +15,10 @@ import * as fs from 'fs';
915
export function activate(context: vscode.ExtensionContext) {
1016
console.log('ObjectQL extension is now active!');
1117

12-
// Register commands
18+
// Initialize Services
19+
objectIndex = new ObjectIndex();
20+
21+
// Register Commands
1322
context.subscriptions.push(
1423
vscode.commands.registerCommand('objectql.newObject', () => createNewFile(context, 'object')),
1524
vscode.commands.registerCommand('objectql.newValidation', () => createNewFile(context, 'validation')),
@@ -18,6 +27,17 @@ export function activate(context: vscode.ExtensionContext) {
1827
vscode.commands.registerCommand('objectql.validateSchema', validateCurrentFile)
1928
);
2029

30+
// Register Providers
31+
const selector = { language: LANGUAGES.YAML, scheme: SCHEMES.FILE };
32+
33+
context.subscriptions.push(
34+
vscode.languages.registerDefinitionProvider(selector, new ObjectDefinitionProvider(objectIndex)),
35+
vscode.languages.registerCompletionItemProvider(selector, new ObjectCompletionProvider(objectIndex), ' ')
36+
);
37+
38+
// Clean up
39+
context.subscriptions.push(objectIndex);
40+
2141
// Show welcome message on first activation
2242
const hasShownWelcome = context.globalState.get('objectql.hasShownWelcome', false);
2343
if (!hasShownWelcome) {
@@ -29,146 +49,10 @@ export function activate(context: vscode.ExtensionContext) {
2949
* Extension deactivation function
3050
*/
3151
export function deactivate() {
32-
console.log('ObjectQL extension is now deactivated');
33-
}
34-
35-
/**
36-
* Create a new ObjectQL file from template
37-
*/
38-
async function createNewFile(context: vscode.ExtensionContext, fileType: 'object' | 'validation' | 'permission' | 'app') {
39-
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
40-
41-
if (!workspaceFolder) {
42-
vscode.window.showErrorMessage('Please open a workspace folder first');
43-
return;
44-
}
45-
46-
// Prompt for filename
47-
const fileName = await vscode.window.showInputBox({
48-
prompt: `Enter ${fileType} name (without extension)`,
49-
placeHolder: `my_${fileType}`,
50-
validateInput: (value: string) => {
51-
if (!value) {
52-
return 'Name cannot be empty';
53-
}
54-
if (!/^[a-z_][a-z0-9_]*$/.test(value)) {
55-
return 'Name must start with lowercase letter or underscore and contain only lowercase letters, numbers, and underscores';
56-
}
57-
return null;
58-
}
59-
});
60-
61-
if (!fileName) {
62-
return;
63-
}
64-
65-
// Determine file path
66-
// Guess the location based on standard folder structure
67-
let folder = 'src';
68-
if (fileType === 'object') {
69-
folder = 'src/objects';
70-
} else if (fileType === 'app') {
71-
folder = 'src';
72-
}
73-
74-
const fullFileName = `${fileName}.${fileType}.yml`;
75-
const defaultPath = path.join(workspaceFolder.uri.fsPath, folder, fullFileName);
76-
77-
// Get template content
78-
const template = getTemplate(context, fileType, fileName);
79-
80-
try {
81-
// Ensure directory exists
82-
const dir = path.dirname(defaultPath);
83-
if (!fs.existsSync(dir)) {
84-
fs.mkdirSync(dir, { recursive: true });
85-
}
86-
87-
// Check if file already exists
88-
if (fs.existsSync(defaultPath)) {
89-
const overwrite = await vscode.window.showWarningMessage(
90-
`File ${fullFileName} already exists. Overwrite?`,
91-
'Yes', 'No'
92-
);
93-
if (overwrite !== 'Yes') {
94-
return;
95-
}
96-
}
97-
98-
// Write file
99-
fs.writeFileSync(defaultPath, template, 'utf8');
100-
101-
// Open file
102-
const document = await vscode.workspace.openTextDocument(defaultPath);
103-
await vscode.window.showTextDocument(document);
104-
105-
vscode.window.showInformationMessage(`Created ${fullFileName}`);
106-
} catch (error) {
107-
vscode.window.showErrorMessage(`Failed to create file: ${error}`);
108-
}
109-
}
110-
111-
/**
112-
* Get template content for file type from template files
113-
*/
114-
function getTemplate(context: vscode.ExtensionContext, fileType: string, name: string): string {
115-
try {
116-
// Check if we are running from 'out' or 'src'
117-
// Usually extension path is the root of the package.
118-
119-
let templatePath = path.join(context.extensionPath, 'src', 'templates', `${fileType}.template.yml`);
120-
121-
// Fallback if not found (maybe flattened or in out)
122-
if (!fs.existsSync(templatePath)) {
123-
templatePath = path.join(context.extensionPath, 'out', 'templates', `${fileType}.template.yml`);
124-
}
125-
126-
if (fs.existsSync(templatePath)) {
127-
let content = fs.readFileSync(templatePath, 'utf8');
128-
content = content.replace(/{{name}}/g, name);
129-
content = content.replace(/{{label}}/g, capitalizeWords(name));
130-
return content;
131-
}
132-
133-
// Fallback to hardcoded string if file read fails (Safety net)
134-
console.warn(`Template file not found at ${templatePath}, utilizing fallback.`);
135-
return getFallbackTemplate(fileType, name);
136-
137-
} catch (e) {
138-
console.error('Error reading template:', e);
139-
return getFallbackTemplate(fileType, name);
140-
}
141-
}
142-
143-
function getFallbackTemplate(fileType: string, name: string): string {
144-
// Minimal fallback
145-
return `# ${capitalizeWords(name)} ${fileType}\nname: ${name}\n`;
146-
}
147-
148-
/**
149-
* Validate current file by saving (triggers schema validation)
150-
*/
151-
async function validateCurrentFile() {
152-
const editor = vscode.window.activeTextEditor;
153-
if (!editor) {
154-
vscode.window.showWarningMessage('No active editor');
155-
return;
156-
}
157-
158-
const document = editor.document;
159-
const fileName = path.basename(document.fileName);
160-
161-
// Check if it's an ObjectQL file
162-
const objectqlFilePattern = /\.(object|validation|permission|app)\.(yml|yaml)$/;
163-
if (!objectqlFilePattern.test(fileName)) {
164-
vscode.window.showWarningMessage('This is not an ObjectQL metadata file');
165-
return;
52+
if (objectIndex) {
53+
objectIndex.dispose();
16654
}
167-
168-
// Trigger validation by saving
169-
await document.save();
170-
171-
vscode.window.showInformationMessage('Validation complete. Check Problems panel for issues.');
55+
console.log('ObjectQL extension is now deactivated');
17256
}
17357

17458
/**
@@ -189,13 +73,3 @@ function showWelcomeMessage(context: vscode.ExtensionContext) {
18973

19074
context.globalState.update('objectql.hasShownWelcome', true);
19175
}
192-
193-
/**
194-
* Capitalize words for display names
195-
*/
196-
function capitalizeWords(str: string): string {
197-
return str
198-
.split('_')
199-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
200-
.join(' ');
201-
}

0 commit comments

Comments
 (0)