New Feature: - Document Symbols provider shows all IFC entities in the Outline view - Entities listed as '#123 IFCWALL' with full definition as detail - Sorted by entity ID for easy navigation - Click any entity in outline to jump directly to that line Implementation: - Added DocumentSymbol and SymbolKind to imports - Enabled documentSymbolProvider capability in server - Provider parses all entities and creates symbols with proper ranges - Uses SymbolKind.Class for entity types The Outline view (bottom of Explorer sidebar) now shows a navigable list of all entities in the IFC file, making it easy to find and jump to specific entities by ID or type.
300 lines
No EOL
11 KiB
JavaScript
300 lines
No EOL
11 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const node_1 = require("vscode-languageserver/node");
|
|
const vscode_languageserver_textdocument_1 = require("vscode-languageserver-textdocument");
|
|
// Create a connection for the server
|
|
const connection = (0, node_1.createConnection)(node_1.ProposedFeatures.all);
|
|
// Create a simple text document manager
|
|
const documents = new node_1.TextDocuments(vscode_languageserver_textdocument_1.TextDocument);
|
|
let hasConfigurationCapability = false;
|
|
let hasWorkspaceFolderCapability = false;
|
|
connection.onInitialize((params) => {
|
|
const capabilities = params.capabilities;
|
|
hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration);
|
|
hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders);
|
|
const result = {
|
|
capabilities: {
|
|
textDocumentSync: node_1.TextDocumentSyncKind.Incremental,
|
|
hoverProvider: true,
|
|
definitionProvider: true,
|
|
documentSymbolProvider: true,
|
|
diagnosticProvider: {
|
|
interFileDependencies: false,
|
|
workspaceDiagnostics: false
|
|
}
|
|
}
|
|
};
|
|
if (hasWorkspaceFolderCapability) {
|
|
result.capabilities.workspace = {
|
|
workspaceFolders: {
|
|
supported: true
|
|
}
|
|
};
|
|
}
|
|
return result;
|
|
});
|
|
connection.onInitialized(() => {
|
|
if (hasConfigurationCapability) {
|
|
connection.client.register(node_1.DidChangeConfigurationNotification.type, undefined);
|
|
}
|
|
if (hasWorkspaceFolderCapability) {
|
|
connection.workspace.onDidChangeWorkspaceFolders(_event => {
|
|
connection.console.log('Workspace folder change event received.');
|
|
});
|
|
}
|
|
connection.console.log('IFC Language Server initialized!');
|
|
});
|
|
const defaultSettings = { maxNumberOfProblems: 1000 };
|
|
let globalSettings = defaultSettings;
|
|
const documentSettings = new Map();
|
|
connection.onDidChangeConfiguration(change => {
|
|
if (hasConfigurationCapability) {
|
|
documentSettings.clear();
|
|
}
|
|
else {
|
|
globalSettings = (change.settings.ifcLanguageServer || defaultSettings);
|
|
}
|
|
connection.languages.diagnostics.refresh();
|
|
});
|
|
function getDocumentSettings(resource) {
|
|
if (!hasConfigurationCapability) {
|
|
return Promise.resolve(globalSettings);
|
|
}
|
|
let result = documentSettings.get(resource);
|
|
if (!result) {
|
|
result = connection.workspace.getConfiguration({
|
|
scopeUri: resource,
|
|
section: 'ifcLanguageServer'
|
|
});
|
|
documentSettings.set(resource, result);
|
|
}
|
|
return result;
|
|
}
|
|
documents.onDidClose(e => {
|
|
documentSettings.delete(e.document.uri);
|
|
});
|
|
function parseIfcEntities(document) {
|
|
const entities = new Map();
|
|
const text = document.getText();
|
|
const lines = text.split('\n');
|
|
// Match lines like: #123=IFCWALL(...)
|
|
const entityPattern = /^#(\d+)=(\w+)\(/;
|
|
lines.forEach((line, index) => {
|
|
const match = entityPattern.exec(line.trim());
|
|
if (match) {
|
|
const id = parseInt(match[1]);
|
|
const type = match[2];
|
|
entities.set(id, {
|
|
id,
|
|
line: index,
|
|
type,
|
|
fullText: line.trim()
|
|
});
|
|
}
|
|
});
|
|
return entities;
|
|
}
|
|
// Get entity ID at cursor position
|
|
// Get entity ID at cursor position
|
|
// Extract all entity references from an entity's definition line
|
|
function extractEntityReferences(entityText) {
|
|
const references = [];
|
|
const pattern = /#(\d+)/g;
|
|
let match;
|
|
while ((match = pattern.exec(entityText)) !== null) {
|
|
const refId = parseInt(match[1]);
|
|
if (!references.includes(refId)) {
|
|
references.push(refId);
|
|
}
|
|
}
|
|
return references;
|
|
}
|
|
// Build hierarchical tree showing entity dependencies
|
|
// Build hierarchical tree showing entity dependencies
|
|
// Build hierarchical tree showing entity dependencies with clickable links
|
|
function buildEntityHierarchy(entityId, entities, visited = new Set(), indent = '') {
|
|
// Prevent infinite loops from circular references
|
|
if (visited.has(entityId)) {
|
|
return `${indent}#${entityId} (circular reference)\n`;
|
|
}
|
|
visited.add(entityId);
|
|
const entity = entities.get(entityId);
|
|
if (!entity) {
|
|
return `${indent}#${entityId} (not found)\n`;
|
|
}
|
|
// Show the full entity definition line (no links)
|
|
let result = `${indent}${entity.fullText}\n`;
|
|
// Get all entities this one references
|
|
const references = extractEntityReferences(entity.fullText);
|
|
// Recursively show each referenced entity
|
|
for (const refId of references) {
|
|
if (refId !== entityId) { // Skip self-references
|
|
result += buildEntityHierarchy(refId, entities, new Set(visited), indent + ' ');
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function getEntityIdAtPosition(document, position) {
|
|
const text = document.getText();
|
|
const offset = document.offsetAt(position);
|
|
connection.console.log(`getEntityIdAtPosition called at line ${position.line}, char ${position.character}`);
|
|
// Get the current line
|
|
const line = document.getText({
|
|
start: { line: position.line, character: 0 },
|
|
end: { line: position.line, character: 1000 }
|
|
});
|
|
connection.console.log(`Current line: "${line}"`);
|
|
// Find all #<number> patterns on this line
|
|
const pattern = /#(\d+)/g;
|
|
let match;
|
|
while ((match = pattern.exec(line)) !== null) {
|
|
const matchStart = match.index;
|
|
const matchEnd = match.index + match[0].length;
|
|
connection.console.log(`Found ${match[0]} at chars ${matchStart}-${matchEnd}, cursor at ${position.character}`);
|
|
// Check if cursor is within this match
|
|
if (position.character >= matchStart && position.character <= matchEnd) {
|
|
const entityId = parseInt(match[1]);
|
|
connection.console.log(`✓ Cursor is on entity #${entityId}`);
|
|
return entityId;
|
|
}
|
|
}
|
|
connection.console.log(`No entity found at cursor position`);
|
|
return null;
|
|
}
|
|
// HOVER: Show entity information
|
|
// HOVER: Show entity hierarchy
|
|
// HOVER: Show entity hierarchy
|
|
// HOVER: Show entity hierarchy
|
|
connection.onHover((params) => {
|
|
const document = documents.get(params.textDocument.uri);
|
|
if (!document)
|
|
return null;
|
|
const entityId = getEntityIdAtPosition(document, params.position);
|
|
if (entityId === null)
|
|
return null;
|
|
const entities = parseIfcEntities(document);
|
|
const entity = entities.get(entityId);
|
|
if (!entity) {
|
|
return {
|
|
contents: `Entity #${entityId} not found in document`
|
|
};
|
|
}
|
|
// Build the hierarchical dependency tree (plain text, no links)
|
|
const hierarchy = buildEntityHierarchy(entityId, entities);
|
|
return {
|
|
contents: `**Entity Hierarchy for #${entityId}**\n\n\`\`\`\n${hierarchy}\`\`\``
|
|
};
|
|
});
|
|
connection.onDefinition((params) => {
|
|
const document = documents.get(params.textDocument.uri);
|
|
if (!document)
|
|
return null;
|
|
const entityId = getEntityIdAtPosition(document, params.position);
|
|
if (entityId === null)
|
|
return null;
|
|
const entities = parseIfcEntities(document);
|
|
const entity = entities.get(entityId);
|
|
if (!entity) {
|
|
connection.console.log(`Entity #${entityId} not found`);
|
|
return null;
|
|
}
|
|
return {
|
|
uri: params.textDocument.uri,
|
|
range: {
|
|
start: { line: entity.line, character: 0 },
|
|
end: { line: entity.line, character: entity.fullText.length }
|
|
}
|
|
};
|
|
});
|
|
// DOCUMENT SYMBOLS: Provide outline view with all entities
|
|
connection.onDocumentSymbol((params) => {
|
|
const document = documents.get(params.textDocument.uri);
|
|
if (!document)
|
|
return [];
|
|
const entities = parseIfcEntities(document);
|
|
const symbols = [];
|
|
// Create a symbol for each entity
|
|
entities.forEach((entity, id) => {
|
|
const range = {
|
|
start: { line: entity.line, character: 0 },
|
|
end: { line: entity.line, character: entity.fullText.length }
|
|
};
|
|
const symbol = {
|
|
name: `#${id} ${entity.type}`,
|
|
detail: entity.fullText,
|
|
kind: node_1.SymbolKind.Class, // Entities are like classes
|
|
range: range,
|
|
selectionRange: range
|
|
};
|
|
symbols.push(symbol);
|
|
});
|
|
// Sort by entity ID
|
|
symbols.sort((a, b) => {
|
|
const idA = parseInt(a.name.match(/#(\d+)/)?.[1] || '0');
|
|
const idB = parseInt(b.name.match(/#(\d+)/)?.[1] || '0');
|
|
return idA - idB;
|
|
});
|
|
return symbols;
|
|
});
|
|
// DIAGNOSTICS: Basic validation (currently minimal)
|
|
connection.languages.diagnostics.on(async (params) => {
|
|
const document = documents.get(params.textDocument.uri);
|
|
if (document !== undefined) {
|
|
return {
|
|
kind: node_1.DocumentDiagnosticReportKind.Full,
|
|
items: await validateIfcDocument(document)
|
|
};
|
|
}
|
|
else {
|
|
return {
|
|
kind: node_1.DocumentDiagnosticReportKind.Full,
|
|
items: []
|
|
};
|
|
}
|
|
});
|
|
async function validateIfcDocument(textDocument) {
|
|
// For now, just basic validation - can be extended later
|
|
const diagnostics = [];
|
|
// Check if file starts with ISO-10303-21
|
|
const text = textDocument.getText();
|
|
if (!text.startsWith('ISO-10303-21')) {
|
|
diagnostics.push({
|
|
severity: node_1.DiagnosticSeverity.Warning,
|
|
range: {
|
|
start: { line: 0, character: 0 },
|
|
end: { line: 0, character: 12 }
|
|
},
|
|
message: 'IFC file should start with ISO-10303-21',
|
|
source: 'ifc-validator'
|
|
});
|
|
}
|
|
return diagnostics;
|
|
}
|
|
documents.onDidChangeContent(change => {
|
|
validateIfcDocument(change.document);
|
|
});
|
|
connection.onDidChangeWatchedFiles(_change => {
|
|
connection.console.log('IFC file change detected');
|
|
});
|
|
// Make the text document manager listen on the connection
|
|
documents.listen(connection);
|
|
// Listen on the connection
|
|
// Debug: log all parsed entities when document opens
|
|
documents.onDidOpen((event) => {
|
|
const doc = event.document;
|
|
connection.console.log(`Document opened: ${doc.uri}`);
|
|
const entities = parseIfcEntities(doc);
|
|
connection.console.log(`Parsed ${entities.size} entities`);
|
|
if (entities.size > 0) {
|
|
connection.console.log("First 10 entities:");
|
|
let count = 0;
|
|
entities.forEach((entity, id) => {
|
|
if (count < 10) {
|
|
connection.console.log(` #${id}: ${entity.type} at line ${entity.line}`);
|
|
count++;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
connection.listen();
|
|
//# sourceMappingURL=server.js.map
|