diff --git a/README.md b/README.md index a06f33a..ab03865 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,9 @@ # format-4690 README -This is the README for your extension "format-4690". After writing up a brief description, we recommend including the following sections. - -## Features - -Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file. - -For example if there is an image subfolder under your extension project workspace: - -\!\[feature X\]\(images/feature-x.png\) - -> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow. - -## Requirements - -If you have any requirements or dependencies, add a section describing those and how to install and configure them. - -## Extension Settings - -Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. - -For example: - -This extension contributes the following settings: - -* `myExtension.enable`: Enable/disable this extension. -* `myExtension.thing`: Set to `blah` to do something. - -## Known Issues - -Calling out known issues can help limit users opening duplicate issues against your extension. - -## Release Notes - -Users appreciate release notes as you update your extension. - -### 1.0.0 - -Initial release of ... - -### 1.0.1 - -Fixed issue #. - -### 1.1.0 - -Added features X, Y, and Z. +Hola --- -## Following extension guidelines +### 2025.06.01 -Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension. - -* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) - -## Working with Markdown - -You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts: - -* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux). -* Toggle preview (`Shift+Cmd+V` on macOS or `Shift+Ctrl+V` on Windows and Linux). -* Press `Ctrl+Space` (Windows, Linux, macOS) to see a list of Markdown snippets. - -## For more information - -* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown) -* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/) - -**Enjoy!** +Inicial diff --git a/language-configuration.json b/language-configuration.json index e42ca25..0253645 100644 --- a/language-configuration.json +++ b/language-configuration.json @@ -23,5 +23,6 @@ ["(", ")"], ["\"", "\""], ["'", "'"] - ] + ], + "wordPattern": "(\\?|[a-zA-Z])([a-zA-Z0-9#\\.]*)[$#%]?" } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 85534d9..a6ae603 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,12 +4,42 @@ interface FunctionDefinition { name: string; location: vscode.Location; type: 'FUNCTION' | 'SUB' | 'DEF'; + parameters: string[]; // Array of parameter names + parameterCount: number; // Number of parameters } -export function activate(context: vscode.ExtensionContext) { +// Global symbol index - maps lowercase function names to their definitions +const symbolIndex = new Map(); + +// File index - maps file URIs to their function definitions +const fileIndex = new Map(); + +export async function activate(context: vscode.ExtensionContext) { const selector: vscode.DocumentSelector = { language: '4690basic' }; - // Existing formatter provider + const saveWatcher = vscode.workspace.onDidSaveTextDocument(async (document) => { + if (document.languageId === '4690basic') { + console.log(`File saved: ${document.uri.toString()}`); + await updateIndexForFile(document.uri); + } + }); + + const fsWatcher = vscode.workspace.createFileSystemWatcher('**/*.{bas,BAS,j86,J86}'); + + fsWatcher.onDidCreate(async (uri) => { + console.log(`File created: ${uri.toString()}`); + await updateIndexForFile(uri); + }); + + fsWatcher.onDidDelete((uri) => { + console.log(`File deleted: ${uri.toString()}`); + removeFromIndex(uri); + }); + + // Build initial symbol index + await buildInitialSymbolIndex(); + + // Existing formatter provider (unchanged) const formatterProvider: vscode.DocumentFormattingEditProvider = { provideDocumentFormattingEdits(document: vscode.TextDocument): vscode.TextEdit[] { const config = vscode.workspace.getConfiguration('4690basic.format'); @@ -26,7 +56,7 @@ export function activate(context: vscode.ExtensionContext) { function removeComments(line: string): string { let inQuotes = false; let quoteChar = ''; - + for (let i = 0; i < line.length; i++) { const char = line[i]; if (!inQuotes && (char === '"' || char === "'")) { @@ -44,20 +74,38 @@ export function activate(context: vscode.ExtensionContext) { // Helper function to check if a line should continue function shouldContinue(line: string, allLines: string[], currentIndex: number): boolean { + const trimmedLine = line.trim(); + + // Skip comment lines that start with backslash (like \REM!!) + // These should not be treated as continuation lines + if (/^\s*\\(REM|rem)/i.test(trimmedLine)) { + return false; + } + const withoutComments = removeComments(line).trim(); - + // Check for explicit continuation with \ - if (withoutComments.endsWith('\\')) { + if (/^(?:[^"\\]|"[^"]*")*\\/.test(withoutComments)) { + // Special case: if this is a THEN \ line, don't continue + // Let the next line be processed as its own logical group + if (/\bTHEN\s*\\/.test(withoutComments)) { + return false; + } return true; } - + // Check for IF without THEN on the same line if (/^\s*IF\b/i.test(withoutComments) && !/\bTHEN\b/i.test(withoutComments)) { return true; } - + // Check if current line looks like a continuation of a condition if (/^\s*(AND\b|OR\b|NOT\b|\()/i.test(withoutComments) && currentIndex > 0) { + // BUT: if this line contains THEN, it terminates the condition, so it should NOT continue + if (/\bTHEN\b/i.test(withoutComments)) { + return false; + } + // Look back to see if we're in a continuation chain let prevIndex = currentIndex - 1; while (prevIndex >= 0) { @@ -66,22 +114,22 @@ export function activate(context: vscode.ExtensionContext) { prevIndex--; continue; } - if (prevLine.endsWith('\\') || - /^\s*IF\b/i.test(prevLine) || + if (/\\.*$/.test(prevLine) || + /^\s*IF\b/i.test(prevLine) || /^\s*(AND\b|OR\b|NOT\b|\()/i.test(prevLine)) { return true; } break; } } - + return false; } while (i < lines.length) { const originalLine = lines[i]; const trimmedLine = originalLine.trim(); - + // Skip empty lines if (trimmedLine === '') { i++; @@ -91,7 +139,7 @@ export function activate(context: vscode.ExtensionContext) { // Collect continuation lines let continuationLines = [originalLine]; let j = i; - + while (j < lines.length && shouldContinue(lines[j], lines, j)) { if (j + 1 < lines.length) { j++; @@ -113,20 +161,40 @@ export function activate(context: vscode.ExtensionContext) { logicalParts.push(part); } } - + const fullLogical = logicalParts.join(' ').trim(); const firstLineTrimmed = removeComments(continuationLines[0]).trim(); + // Check if this line follows a THEN \ continuation + const isPrevThenContinuation = i > 0 && /\bTHEN\s*\\/.test(removeComments(lines[i - 1])); + + // Check if this is a THEN continuation that starts a new block + const isThenContinuation = /\bTHEN\s*\\/.test(removeComments(continuationLines[0])); + let thenBlockStarts = false; + + if (isThenContinuation && continuationLines.length > 1) { + // Check if the continuation contains an opening construct + for (let k = 1; k < continuationLines.length; k++) { + const contLine = removeComments(continuationLines[k]).trim(); + if (/^\s*(FUNCTION|DEF|SUB|WHILE|FOR|IF|BEGIN)\b/i.test(contLine)) { + thenBlockStarts = true; + break; + } + } + } + // Simple approach: look for keywords that affect indentation const containsBegin = /\bBEGIN\b/i.test(fullLogical); - const containsNext = /\bNEXT\b/i.test(fullLogical); - - const isClosing = /^\s*(END FUNCTION|FEND|END SUB|ENDIF|WEND|NEXT)\b/i.test(firstLineTrimmed) || + const containsNext = /^\s*NEXT\b/i.test(firstLineTrimmed); + + const isClosing = /^\s*(END\s+FUNCTION|FEND|END\s+SUB|ENDIF|WEND|NEXT)\b/i.test(firstLineTrimmed) || containsNext || (/^\s*ELSE\b/i.test(firstLineTrimmed) && !containsBegin); const isOpening = /^\s*(FUNCTION|DEF|SUB|WHILE|FOR)\b/i.test(fullLogical) || - containsBegin; + /^\s*(FUNCTION|DEF|SUB|WHILE|FOR)\b/i.test(firstLineTrimmed) || + containsBegin || + thenBlockStarts; // Handle compound statements like ENDIF ELSE BEGIN const isCompoundEndifElse = /\bENDIF\b.*\bELSE\s+BEGIN\b/i.test(fullLogical); @@ -147,7 +215,25 @@ export function activate(context: vscode.ExtensionContext) { if (trimmed === '') continue; - const indent = indentUnit.repeat(indentLevel); + // For continuation lines (k > 0), use appropriate indentation + let lineIndentLevel = indentLevel; + + if (isPrevThenContinuation) { + // This line follows a THEN \ - it should be indented + lineIndentLevel = indentLevel + 1; + } else if (k > 0 && isThenContinuation) { + // This is a continuation after THEN - it should be indented + lineIndentLevel = indentLevel + 1; + } else if (k > 0) { + // Regular continuation line - check if the previous line ended with \ + const prevLine = removeComments(continuationLines[k - 1]).trim(); + if (prevLine.endsWith('\\')) { + // Keep the same indentation as the main statement + lineIndentLevel = indentLevel; + } + } + + const indent = indentUnit.repeat(lineIndentLevel); const formatted = indent + trimmed; if (formatted !== original) { @@ -181,7 +267,7 @@ export function activate(context: vscode.ExtensionContext) { function removeComments(line: string): string { let inQuotes = false; let quoteChar = ''; - + for (let i = 0; i < line.length; i++) { const char = line[i]; if (!inQuotes && (char === '"' || char === "'")) { @@ -200,12 +286,12 @@ export function activate(context: vscode.ExtensionContext) { // Helper function to check if a line should continue function shouldContinue(line: string, allLines: string[], currentIndex: number): boolean { const withoutComments = removeComments(line).trim(); - + // Check for explicit continuation with \ if (withoutComments.endsWith('\\')) { return true; } - + // For function definitions, also check if we have incomplete parentheses if (/^\s*(FUNCTION|DEF|SUB)\b/i.test(withoutComments)) { const openParens = (withoutComments.match(/\(/g) || []).length; @@ -214,15 +300,38 @@ export function activate(context: vscode.ExtensionContext) { return true; } } - + return false; } + // Helper function to parse parameters from a parameter string + function parseParameters(paramString: string): string[] { + if (!paramString.trim()) { + return []; + } + + const parameters: string[] = []; + const parts = paramString.split(','); + + for (const part of parts) { + const trimmedPart = part.trim(); + if (trimmedPart) { + // Extract parameter name using the identifier regex + const match = identifierRegex.exec(trimmedPart); + if (match) { + parameters.push(match[0]); + } + } + } + + return parameters; + } + let i = 0; while (i < lines.length) { const originalLine = lines[i]; const trimmedLine = originalLine.trim(); - + // Skip empty lines if (trimmedLine === '') { i++; @@ -239,7 +348,7 @@ export function activate(context: vscode.ExtensionContext) { // Collect continuation lines for this function definition let continuationLines = [originalLine]; let j = i; - + while (j < lines.length && shouldContinue(lines[j], lines, j)) { if (j + 1 < lines.length) { j++; @@ -261,27 +370,41 @@ export function activate(context: vscode.ExtensionContext) { logicalParts.push(part); } } - + const fullLogical = logicalParts.join(' ').trim(); - + // Check if this is not an external declaration if (!/\bEXTERNAL\s*$/i.test(fullLogical)) { // Extract the function type and name const defType = functionMatch[1].toUpperCase() as 'FUNCTION' | 'SUB' | 'DEF'; - + // Find the identifier after the function keyword const afterKeyword = fullLogical.substring(functionMatch[0].length).trim(); const nameMatch = identifierRegex.exec(afterKeyword); - + if (nameMatch) { const functionName = nameMatch[0]; const position = new vscode.Position(i, originalLine.indexOf(functionName)); const location = new vscode.Location(document.uri, position); - + + // Extract parameters + let parameters: string[] = []; + const afterName = afterKeyword.substring(nameMatch[0].length).trim(); + + // Check if there are parentheses after the function name + const parenMatch = /^\s*\(\s*(.*?)\s*\)/.exec(afterName); + if (parenMatch) { + // Extract parameter string and parse it + const paramString = parenMatch[1]; + parameters = parseParameters(paramString); + } + definitions.push({ name: functionName, location: location, - type: defType + type: defType, + parameters: parameters, + parameterCount: parameters.length }); } } @@ -293,7 +416,84 @@ export function activate(context: vscode.ExtensionContext) { return definitions; } - // Definition provider - searches all files in workspace + // Function to build the initial symbol index + async function buildInitialSymbolIndex(): Promise { + try { + console.log('Building symbol index...'); + const workspaceFiles = await vscode.workspace.findFiles('**/*.{bas,BAS,j86,J86}'); + + for (const fileUri of workspaceFiles) { + try { + await updateIndexForFile(fileUri); + } catch (error) { + console.warn(`Failed to index file ${fileUri.toString()}:`, error); + } + } + + console.log(`Symbol index built with ${symbolIndex.size} unique symbols across ${fileIndex.size} files`); + } catch (error) { + console.error('Failed to build initial symbol index:', error); + } + } + + // Function to update the index for a specific file + async function updateIndexForFile(fileUri: vscode.Uri): Promise { + try { + const document = await vscode.workspace.openTextDocument(fileUri); + const definitions = parseFunctionDefinitions(document); + const fileKey = fileUri.toString(); + + // Remove old definitions from the global symbol index + const oldDefinitions = fileIndex.get(fileKey) || []; + for (const oldDef of oldDefinitions) { + const symbolKey = oldDef.name.toLowerCase(); + const symbolDefs = symbolIndex.get(symbolKey) || []; + const filteredDefs = symbolDefs.filter(def => def.location.uri.toString() !== fileKey); + + if (filteredDefs.length === 0) { + symbolIndex.delete(symbolKey); + } else { + symbolIndex.set(symbolKey, filteredDefs); + } + } + + // Add new definitions to the indexes + fileIndex.set(fileKey, definitions); + + for (const definition of definitions) { + const symbolKey = definition.name.toLowerCase(); + const existing = symbolIndex.get(symbolKey) || []; + existing.push(definition); + symbolIndex.set(symbolKey, existing); + } + } catch (error) { + console.warn(`Failed to update index for file ${fileUri.toString()}:`, error); + } + } + + // Function to remove a file from the index + function removeFromIndex(fileUri: vscode.Uri): void { + const fileKey = fileUri.toString(); + const definitions = fileIndex.get(fileKey) || []; + + // Remove from symbol index + for (const definition of definitions) { + const symbolKey = definition.name.toLowerCase(); + const symbolDefs = symbolIndex.get(symbolKey) || []; + const filteredDefs = symbolDefs.filter(def => def.location.uri.toString() !== fileKey); + + if (filteredDefs.length === 0) { + symbolIndex.delete(symbolKey); + } else { + symbolIndex.set(symbolKey, filteredDefs); + } + } + + // Remove from file index + fileIndex.delete(fileKey); + } + + // Optimized definition provider - uses the symbol index const definitionProvider: vscode.DefinitionProvider = { async provideDefinition( document: vscode.TextDocument, @@ -305,66 +505,44 @@ export function activate(context: vscode.ExtensionContext) { return undefined; } - const word = document.getText(wordRange); - - // Search in current document first - const currentDocDefs = parseFunctionDefinitions(document); - const localDef = currentDocDefs.find(def => def.name.toLowerCase() === word.toLowerCase()); + const word = document.getText(wordRange).toLowerCase(); + const definitions = symbolIndex.get(word); + + if (!definitions || definitions.length === 0) { + return undefined; + } + + // If multiple definitions exist, prefer the one in the current document + const localDef = definitions.find(def => def.location.uri.toString() === document.uri.toString()); if (localDef) { return localDef.location; } - // Search in all workspace files - const workspaceFiles = await vscode.workspace.findFiles('**/*.{bas,BAS,j86,J86}'); - - for (const fileUri of workspaceFiles) { - if (fileUri.toString() === document.uri.toString()) { - continue; // Skip current document as we already searched it - } - - try { - const doc = await vscode.workspace.openTextDocument(fileUri); - const definitions = parseFunctionDefinitions(doc); - const definition = definitions.find(def => def.name.toLowerCase() === word.toLowerCase()); - - if (definition) { - return definition.location; - } - } catch (error) { - // Skip files that can't be opened - continue; - } - } - - return undefined; + // Otherwise return the first definition found + return definitions[0].location; } }; - // Workspace symbol provider - only searches open documents + // Updated workspace symbol provider - uses the symbol index const workspaceSymbolProvider: vscode.WorkspaceSymbolProvider = { async provideWorkspaceSymbols( query: string, token: vscode.CancellationToken ): Promise { const symbols: vscode.SymbolInformation[] = []; - - // Only search in currently open documents - for (const document of vscode.workspace.textDocuments) { - // Only search in files with our language ID - if (document.languageId === '4690basic') { - const definitions = parseFunctionDefinitions(document); - + const queryLower = query.toLowerCase(); + + for (const [symbolName, definitions] of symbolIndex) { + if (!query || symbolName.includes(queryLower)) { for (const def of definitions) { - if (!query || def.name.toLowerCase().includes(query.toLowerCase())) { - const symbolKind = def.type === 'SUB' ? vscode.SymbolKind.Method : vscode.SymbolKind.Function; - const symbol = new vscode.SymbolInformation( - def.name, - symbolKind, - '', - def.location - ); - symbols.push(symbol); - } + const symbolKind = def.type === 'SUB' ? vscode.SymbolKind.Method : vscode.SymbolKind.Function; + const symbol = new vscode.SymbolInformation( + def.name, + symbolKind, + '', + def.location + ); + symbols.push(symbol); } } } @@ -373,34 +551,214 @@ export function activate(context: vscode.ExtensionContext) { } }; - // Document symbol provider (for outline view) + // Enhanced Document Symbol Provider that provides hierarchical symbols for breadcrumbs const documentSymbolProvider: vscode.DocumentSymbolProvider = { provideDocumentSymbols( document: vscode.TextDocument, token: vscode.CancellationToken ): vscode.DocumentSymbol[] { - const definitions = parseFunctionDefinitions(document); - const symbols: vscode.DocumentSymbol[] = []; - - for (const def of definitions) { - const symbolKind = def.type === 'SUB' ? vscode.SymbolKind.Method : vscode.SymbolKind.Function; - const range = new vscode.Range(def.location.range.start, def.location.range.start); - - const symbol = new vscode.DocumentSymbol( - def.name, - def.type, - symbolKind, - range, - range - ); - symbols.push(symbol); - } - - return symbols; + return parseDocumentSymbolsWithRanges(document); } }; - // Hover provider to help VS Code recognize symbols and show proper highlighting + // Function to parse document symbols with proper ranges for breadcrumbs + function parseDocumentSymbolsWithRanges(document: vscode.TextDocument): vscode.DocumentSymbol[] { + const symbols: vscode.DocumentSymbol[] = []; + const lines = document.getText().split('\n'); + const identifierRegex = /(\?|[a-zA-Z])([a-zA-Z0-9#\.]*)[\$#%]?/; + + // Helper function to check if a keyword appears after a backslash or comment marker + function isKeywordCommentedOut(line: string, keywordMatch: RegExpExecArray): boolean { + const keywordStart = keywordMatch.index!; + let inQuotes = false; + + for (let i = 0; i < keywordStart; i++) { + const char = line[i]; + if (!inQuotes && char === '"') { + inQuotes = true; + } else if (inQuotes && char === '"') { + inQuotes = false; + } else if (!inQuotes && (char === '\\' || char === '!')) { + // Found a comment marker before the keyword, so keyword is commented out + return true; + } + } + return false; + } + + // Helper function to parse parameters from a parameter string + function parseParameters(paramString: string): string[] { + if (!paramString.trim()) { + return []; + } + + const parameters: string[] = []; + const parts = paramString.split(','); + + for (const part of parts) { + const trimmedPart = part.trim(); + if (trimmedPart) { + // Extract parameter name using the identifier regex + const match = identifierRegex.exec(trimmedPart); + if (match) { + parameters.push(match[0]); + } + } + } + + return parameters; + } + + // Find the end of a function/subroutine starting from a given line + function findFunctionEnd(startLine: number, functionType: 'FUNCTION' | 'DEF' | 'SUB'): number { + let endPattern: RegExp; + + // console.log('functionType: ', functionType); + + if (functionType === 'FUNCTION' || functionType === 'DEF') { + endPattern = /^\s*(END\s+FUNCTION)|(FEND)\b/i; + } else { // SUB + endPattern = /^\s*END\s+SUB\b/i; + } + + // console.log('endPattern: ', endPattern); + + for (let i = startLine + 1; i < lines.length; i++) { + const line = lines[i]; + + // Skip commented lines + if (/^\s*(!|\\)/.test(line)) { + continue; + } + + // Skip empty lines + if (line.trim() === '') { + continue; + } + + // console.log('Evaluating for end: ', line); + + // Check if this line matches the end pattern + const endMatch = endPattern.exec(line); + if (endMatch) { + + // console.log('Found match: ', endMatch); + + // Check if the end keyword is not commented out + if (!isKeywordCommentedOut(line, endMatch)) { + // console.log('Returning i'); + return i; + } + } + } + + // If no end found, return the last line of the document + return lines.length - 1; + } + + // Process each line looking for function/sub definitions + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmedLine = line.trim(); + + + // Skip empty lines + if (trimmedLine === '') { + continue; + } + + // Skip commented lines + if (/^\s*(!|\\)/.test(trimmedLine)) { + continue; + } + + // Check if this line starts a function definition + const functionMatch = /^\s*(FUNCTION|DEF|SUB)\s+/i.exec(line); + if (!functionMatch) { + continue; + } + + // Skip if the function keyword is commented out + if (isKeywordCommentedOut(line, functionMatch)) { + continue; + } + + // console.log('Current line: ', trimmedLine); + + // Extract the function type and name + const defType = functionMatch[1].toUpperCase() as 'FUNCTION' | 'SUB' | 'DEF'; + + // console.log('functionMatch: ', functionMatch); + // console.log('Definition type: ', defType); + + // Find the identifier after the function keyword + const afterKeyword = line.substring(functionMatch[0].length).trim(); + const nameMatch = identifierRegex.exec(afterKeyword); + + // console.log('afterKeyword: ', afterKeyword); + // console.log('nameMatch: ', nameMatch); + + if (!nameMatch) { + continue; + } + + // Check if this is not an external declaration + if (/\bEXTERNAL\s*.*$/i.test(line)) { + continue; + } + + const functionName = nameMatch[0]; + const startPosition = new vscode.Position(i, line.indexOf(functionMatch[1])); + + // Find the end of this function + const endLineIndex = findFunctionEnd(i, defType); + const endPosition = new vscode.Position(endLineIndex, lines[endLineIndex].length); + + // Create the full range for the function + const fullRange = new vscode.Range(startPosition, endPosition); + + // Create a selection range (just the function name) + const nameStartCol = line.indexOf(functionName); + const namePosition = new vscode.Position(i, nameStartCol); + const nameEndPosition = new vscode.Position(i, nameStartCol + functionName.length); + const selectionRange = new vscode.Range(namePosition, nameEndPosition); + + // Extract parameters + let parameters: string[] = []; + const afterName = afterKeyword.substring(nameMatch[0].length).trim(); + + // Check if there are parentheses after the function name + const parenMatch = /^\s*\(\s*(.*?)\s*\)/.exec(afterName); + if (parenMatch) { + // Extract parameter string and parse it + const paramString = parenMatch[1]; + parameters = parseParameters(paramString); + } + + // Create symbol kind + const symbolKind = defType === 'SUB' ? vscode.SymbolKind.Method : vscode.SymbolKind.Function; + + // Create detail string with parameters + let detail = defType; + if (parameters.length > 0) { + detail += `(${parameters.join(', ')})`; + } + + const symbol = new vscode.DocumentSymbol( + functionName, + detail, + symbolKind, + fullRange, + selectionRange + ); + + symbols.push(symbol); + } + + return symbols; + } + + // Updated hover provider - now shows parameter information const hoverProvider: vscode.HoverProvider = { provideHover( document: vscode.TextDocument, @@ -412,40 +770,30 @@ export function activate(context: vscode.ExtensionContext) { return undefined; } - const word = document.getText(wordRange); - - // Search for the function definition in current document - const currentDocDefs = parseFunctionDefinitions(document); - let definition = currentDocDefs.find(def => def.name.toLowerCase() === word.toLowerCase()); - - // If not found locally, search in open documents - if (!definition) { - for (const openDoc of vscode.workspace.textDocuments) { - if (openDoc.uri.toString() === document.uri.toString()) { - continue; - } - - if (openDoc.languageId === '4690basic') { - const definitions = parseFunctionDefinitions(openDoc); - definition = definitions.find(def => def.name.toLowerCase() === word.toLowerCase()); - - if (definition) { - break; - } - } - } + const word = document.getText(wordRange).toLowerCase(); + const definitions = symbolIndex.get(word); + + if (!definitions || definitions.length === 0) { + return undefined; } - if (definition) { - const typeLabel = definition.type === 'SUB' ? 'Subroutine' : 'Function'; - const content = new vscode.MarkdownString(); - content.appendCodeblock(`${definition.type} ${definition.name}`, '4690basic'); - content.appendText(`\n${typeLabel}: ${definition.name}`); - - return new vscode.Hover(content, wordRange); + // Prefer definition from current document, otherwise use first one + const definition = definitions.find(def => def.location.uri.toString() === document.uri.toString()) || definitions[0]; + + const content = new vscode.MarkdownString(); + + // Build function signature with parameters + let signature = `${definition.type} ${definition.name}`; + if (definition.parameterCount > 0) { + signature += `(${definition.parameters.join(', ')})`; } - return undefined; + content.appendCodeblock(signature, '4690basic'); + + const path = vscode.workspace.name ? definition.location.uri.path.slice(definition.location.uri.path.indexOf(vscode.workspace.name)) : 'Archivo fuente no encontrado'; + content.appendMarkdown(`***\n\`${path}\``); + + return new vscode.Hover(content, wordRange); } }; @@ -455,8 +803,14 @@ export function activate(context: vscode.ExtensionContext) { vscode.languages.registerDefinitionProvider(selector, definitionProvider), vscode.languages.registerWorkspaceSymbolProvider(workspaceSymbolProvider), vscode.languages.registerDocumentSymbolProvider(selector, documentSymbolProvider), - vscode.languages.registerHoverProvider(selector, hoverProvider) + vscode.languages.registerHoverProvider(selector, hoverProvider), + saveWatcher, + fsWatcher ); } -export function deactivate() {} \ No newline at end of file +export function deactivate() { + // Clear the indexes on deactivation + symbolIndex.clear(); + fileIndex.clear(); +} \ No newline at end of file diff --git a/syntaxes/4690basic.tmLanguage.json b/syntaxes/4690basic.tmLanguage.json index 56fbbba..34154c0 100644 --- a/syntaxes/4690basic.tmLanguage.json +++ b/syntaxes/4690basic.tmLanguage.json @@ -3,23 +3,15 @@ "name": "4690 BASIC", "patterns": [ { - "name": "string.quoted.double", - "begin": "\"", - "end": "\"", - "patterns": [ - { - "match": ".", - "name": "string.quoted.double" - } - ] + "include": "#comments" + }, + { + "include": "#strings" }, { "name": "string.regexp", "match": "\\b[\\w\\.]+:" }, - { - "include": "#comments" - }, { "include": "#function-decl" }, @@ -44,12 +36,6 @@ { "include": "#math-function" }, - { - "include": "#string-function" - }, - { - "include": "#variable-assignment" - }, { "include": "#if-statement" }, @@ -74,9 +60,6 @@ { "include": "#keywords" }, - { - "include": "#strings" - }, { "include": "#stmt-w-parameters" }, @@ -98,11 +81,27 @@ { "include": "#number-literal" }, + { + "include": "#string-function" + }, { "include": "#identifier" } ], "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line", + "match": "!.*|\\bREM.*|\\bREMARK.*" + } + ] + }, + "strings": { + "name": "string.quoted.double", + "begin": "\"", + "end": "\"" + }, "line-continuation": { "name": "meta.line.continuation", "match": "\\\\(.*)\\n", @@ -112,14 +111,6 @@ } } }, - "comments": { - "patterns": [ - { - "name": "comment.line", - "match": "!.*|REM.*|REMARK.*" - } - ] - }, "function-decl": { "name": "meta.function", "begin": "(?i)\\b(FUNCTION|DEF)\\s+((\\?|[a-zA-Z])([a-zA-Z0-9#.]*)[\\$#%]?)", @@ -184,7 +175,7 @@ }, "subroutine-end": { "name": "keyword.control", - "match": "END SUB|end sub" + "match": "(?i)END\\s+SUB" }, "integer-decl": { "begin": "(INTEGER|integer)((\\*)(1|2|4))?", @@ -206,7 +197,7 @@ }, { "name": "entity.name.function", - "match": "(\\?|[a-zA-Z])([a-zA-Z0-9#.]*)([\\$#%])?", + "match": "(\\?|[a-zA-Z])([a-zA-Z0-9#\\.]*)([\\$#%])?", "captures": { "3": { "name": "keyword.operator" @@ -283,7 +274,7 @@ } }, "real-decl": { - "begin": "REAL|real", + "begin": "(?i)REAL\\s+", "beginCaptures": { "0": { "name": "storage.type" @@ -365,25 +356,7 @@ }, "math-function": { "name": "support.function.arithmetic", - "match": "(?i)\\b(ABS|CHR\\$|FLOAT|INT%?|MOD|PEEK|SGN|SHIFT|STR\\$|TAB)\\b[^%\\$\\.]" - }, - "variable-assignment": { - "name": "meta.assignment", - "match": "^\\s*(\\?|[a-zA-Z])([a-zA-Z0-9#.]*)([\\$#%])?\\s+(=)", - "captures": { - "1": { - "name": "variable.name" - }, - "2": { - "name": "variable.name" - }, - "3": { - "name": "keyword.operator" - }, - "4": { - "name": "keyword.operator.arithmetic" - } - } + "match": "(?i)\\b(ABS|CHR\\$|FLOAT|INT%?|MOD|PEEK|SGN|SHIFT|STR\\$|TAB)(?=\\s*\\()" }, "numeric-expression": { "name": "meta.expression.numeric", @@ -405,24 +378,28 @@ ] }, "identifier": { - "patterns": [ - { - "name": "variable.name", - "match": "\\b(\\?|[a-zA-Z])([a-zA-Z0-9#.]*)[\\$#%]?\\b" + "name": "meta.identifier", + "match": "((\\?|[a-zA-Z])([a-zA-Z0-9#\\.]*))([\\$#%])?", + "captures": { + "1": { + "name": "variable.name" + }, + "4": { + "name": "keyword.operator" } - ] + } }, "number-literal": { "patterns": [ { "name": "constant.numeric", - "match": "(?i)\\b([+-]?[0-9.]+)(E[+-]?[0-9]+)?[hb]?\\b" + "match": "(?i)\\b([+-]?[0-9\\.ABCDEF]+)(E[+-]?[0-9]+)?[HB]?\\b" } ] }, "arithmetic-operator": { "name": "keyword.operator.arithmetic", - "match": "[\\+-\\/\\^\\*]" + "match": "[\\+-\\/\\^\\*#]" }, "relational-operator": { "name": "keyword.operator.relational", @@ -430,27 +407,17 @@ }, "logical-operator": { "name": "keyword.operator.logical", - "match": "\\b(NOT|AND|OR|XOR)\\b" + "match": "(?i)\\b(NOT|AND|OR|XOR)\\b" }, "string-operator": { "name": "keyword.operator", "match": "[+]" }, "string-function": { - "begin": "(?i)\\b(ASC|LEN|PACK\\$|TRANSLATE\\$|UCASE\\$|UNPACK\\$|VAL|MID\\$|LEFT\\$|RIGHT\\$|MATCH)\\b", - "beginCaptures": { - "1": { - "name": "support.function.string" - }, - "0": { - "name": "meta.function-call.begin" - } - }, - "end": "\\(", - "endCaptures": { - "0": { - "name": "punctuation.parameters.start" - } + "name": "support.function.builtin", + "match": "(?i)\\b(ASC|LEN|PACK\\$|TRANSLATE\\$|UCASE\\$|UNPACK\\$|VAL|MID\\$|LEFT\\$|RIGHT\\$|MATCH|SUBSTR|STRING\\$)(?=\\s*\\()", + "captures": { + "1": { "name": "support.function.builtin"} } }, "string-expression": { @@ -542,7 +509,7 @@ "patterns": [ { "name": "keyword.control", - "match": "(?i)\\b(GOSUB|RETURN|GOTO|WHILE|WEND|NEXT|ON|ERROR|STOP|RANDOMIZE|CHAIN|COMMON|CALL|EXIT SUB|FORM)\\b[^%\\$\\.]" + "match": "(?i)\\b(GOSUB|RETURN|GOTO|WHILE|WEND|NEXT|ON|ERROR|STOP|RANDOMIZE|CHAIN|COMMON|CALL|EXIT\\s+SUB|EXIT\\s+FUNCTION|FORM)\\b[^%\\$\\.]" }, { "name": "support.function", @@ -550,17 +517,6 @@ } ] }, - "strings": { - "name": "string.quoted.double.4690basic", - "begin": "\"", - "end": "\"", - "patterns": [ - { - "name": "constant.character.escape.4690basic", - "match": "\\\\." - } - ] - }, "single-stmts": { "name": "support.function", "match": "(COMMAND$|CONCHAR%|CONSOLE|DATE$)" @@ -585,24 +541,16 @@ { "include": "#identifier" }, - { - "name": "punctuation.separator.parameter", - "match": "," - }, - { - "name": "punctuation.separator.option", - "match": ";" - }, { "match": "\\s+", "name": "text.whitespace" } ], - "end": "$" + "end": "(? https://webpack.js.org/configuration/node/ - mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') + mode: 'production', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ output: {