ifc-language-server/server/out/server.js
Ryan Schultz e564abdc7b feat: Add Document Symbols provider for Outline view
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.
2025-12-07 13:42:13 -06:00

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