From f5cfa710e800129b2f146af13dffa18beee0f11b Mon Sep 17 00:00:00 2001 From: "R.Sinthujan" Date: Fri, 18 Jul 2025 17:51:16 +0530 Subject: [PATCH 1/2] minor issues resolved --- api/api.js | 4 +- extension.js | 198 +++++++++++++++++++++++++++++++++++++++---- utils/checkErrors.js | 19 +++++ 3 files changed, 204 insertions(+), 17 deletions(-) create mode 100644 utils/checkErrors.js diff --git a/api/api.js b/api/api.js index c4c5000..7a792d2 100644 --- a/api/api.js +++ b/api/api.js @@ -1,4 +1,6 @@ const axios = require("axios"); module.exports = axios.create({ baseURL: "http://127.0.0.1:8000", -}); \ No newline at end of file +}); + + diff --git a/extension.js b/extension.js index 09195be..9b81f7f 100644 --- a/extension.js +++ b/extension.js @@ -9,6 +9,7 @@ const debounce = require('./utils/debounce'); const { PerlCodebaseIndexer } = require('./indexers/codebaseIndexer'); const { PerlRepositoryMapProvider } = require('./collectors/repoMapProvider'); const { registerCommands } = require('./commands/commands') +const checkCodeForErrors = require('./utils/checkErrors') /** * Global extension configuration @@ -16,34 +17,152 @@ const { registerCommands } = require('./commands/commands') const config = { // Default values, will be overridden by user settings relevantCodeCount: 2, + // NEW: Debounce time for error checking (in milliseconds) + errorCheckDebounceTime: 2000, // 2 seconds, you can set this to 10000 for 10 seconds useMemoryIndex:true, indexOnStartup: true, contextWindowSize: 15, // lines around cursor to consider for context }; -// Global state -let codebaseIndexer = null; -let outputChannel = null; -let debounceTime = 2000; +// Global state for tracking in-flight requests +const inFlightRequests = new Map(); + + /** - * Fetches code suggestion based on a comment + * Creates a unique key for a request based on comment and context * @param {string} comment - The user's comment * @param {vscode.TextDocument} doc - Current document * @param {vscode.Position} pos - Current cursor position - * @returns {Promise} Generated code suggestion + * @returns {string} Unique key for the request */ -async function fetchCode(comment, doc, pos) { - try { - const ctx = await generateContextForComments(comment, doc, pos); - const response = await api.post('/commentCode/', { message: comment, context: ctx }); - return response.data.code; - } catch (err) { - logError(`Error fetching suggestion: ${err.message}`, err); - vscode.window.showErrorMessage(`Failed to generate code: ${err.message}`); - return null; +function createRequestKey(comment, doc, pos) { + // Create a unique key that represents this specific request + return `${doc.fileName}:${pos.line}:${pos.character}:${comment.trim()}`; +} + +// Global state +let codebaseIndexer = null; +let outputChannel = null; +let debounceTime = 3000; +// NEW: Diagnostic collection for displaying errors +let errorDiagnostics = null; +// /** +// * Fetches code suggestion based on a comment +// * @param {string} comment - The user's comment +// * @param {vscode.TextDocument} doc - Current document +// * @param {vscode.Position} pos - Current cursor position +// * @returns {Promise} Generated code suggestion +// */ +// async function fetchCode(comment, doc, pos) { +// try { +// const ctx = await generateContextForComments(comment, doc, pos); +// const response = await api.post('/commentCode/', { message: comment, context: ctx }); +// return response.data.code; +// } catch (err) { +// logError(`Error fetching suggestion: ${err.message}`, err); +// vscode.window.showErrorMessage(`Failed to generate code: ${err.message}`); +// return null; +// } +// } + +async function fetchCodeWithDeduplication(comment, doc, pos) { + const requestKey = createRequestKey(comment, doc, pos); + + // Check if we already have a request in progress for this exact same input + if (inFlightRequests.has(requestKey)) { + logDebug(`Returning existing promise for request: ${requestKey}`); + return inFlightRequests.get(requestKey); } + + // Create new promise for this request + const requestPromise = (async () => { + try { + logDebug(`Starting new request: ${requestKey}`); + const ctx = await generateContextForComments(comment, doc, pos); + const response = await api.post('/commentCode/', { message: comment, context: ctx }); + logDebug(`Request completed: ${requestKey}`); + return response.data.code; + } catch (err) { + logError(`Error fetching suggestion for ${requestKey}: ${err.message}`, err); + vscode.window.showErrorMessage(`Failed to generate code: ${err.message}`); + return null; + } finally { + // Always clean up the request from the map when it's done + inFlightRequests.delete(requestKey); + logDebug(`Cleaned up request: ${requestKey}`); + } + })(); + + // Store the promise in our map + inFlightRequests.set(requestKey, requestPromise); + + return requestPromise; +} + + +/** + * NEW: Analyzes the entire document for errors and updates diagnostics + * @param {vscode.TextDocument} doc - The document to check + */ +async function updateErrorDiagnostics(doc) { + if (doc.languageId !== 'perl') { + return; // Only check Perl files + } + + logInfo(`Running error check for: ${doc.fileName}`); + try { + const code = doc.getText(); + const lines = code.split('\n'); + + // Create array of lines with their line numbers, including empty lines + // Create formatted string with padded line numbers + const totalLines = lines.length; + const padding = totalLines.toString().length; + const linesWithNumbers = lines + .map((text, index) => { + const lineNumber = (index + 1).toString().padStart(padding, ' '); + return `${lineNumber}: ${text}`; + }) + .join('\n'); + + + + // Send code with line numbers to backend + const response = await checkCodeForErrors(linesWithNumbers); + const errors = response.data.errors; // Assuming the errors are in response.data.errors + + if (!Array.isArray(errors)) { + logError("Received invalid error format from API.", errors); + return; + } + + const diagnostics = errors.map(error => { + // VS Code lines are 0-indexed, API might return 1-indexed + const line = Math.max(0, error.line - 1); + const startChar = error.start || 0; + const endChar = error.end || Math.max(startChar + 1, lines[line]?.length || 0); + + const range = new vscode.Range( + new vscode.Position(line, startChar), + new vscode.Position(line, endChar) + ); + + const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error); + diagnostic.source = 'Perl AI Assistant'; + return diagnostic; + }); + + errorDiagnostics.set(doc.uri, diagnostics); + logInfo(`Found ${diagnostics.length} errors.`); + + } catch (err) { + logInfo(`Failed to check for errors: ${err.message}`, err); + // Do not show an error message to the user to avoid being disruptive + } } + + /** * Collects and generates context for AI code generation * @param {string} comment - The user's comment @@ -226,8 +345,55 @@ async function activate(context) { }); }, 300); + // Create debounced version of fetchCode - const debouncedFetch = debounce(fetchCode, debounceTime); + const debouncedFetch = debounce(fetchCodeWithDeduplication, debounceTime); + + // NEW: Create a debounced version of the error checker + const debouncedErrorCheck = debounce( + (doc) => updateErrorDiagnostics(doc), + config.errorCheckDebounceTime + ); + logInfo("Step 4: Debounced functions created."); + + // NEW: Initialize Diagnostics Collection + logInfo("Step 5: Initializing diagnostics collection..."); + errorDiagnostics = vscode.languages.createDiagnosticCollection("perl-ai-errors"); + context.subscriptions.push(errorDiagnostics); + logInfo("Step 5: Diagnostics collection initialized."); + + // NEW: Register event listener for when a text document is changed + logInfo("Step 6: Registering event listeners..."); + context.subscriptions.push( + vscode.workspace.onDidChangeTextDocument(event => { + if (vscode.window.activeTextEditor && event.document === vscode.window.activeTextEditor.document) { + debouncedErrorCheck(event.document); + } + }) + ); + + // NEW: Register event listener for when the active editor changes + context.subscriptions.push( + vscode.window.onDidChangeActiveTextEditor(editor => { + if (editor) { + // Trigger an immediate check when switching to a new file + debouncedErrorCheck(editor.document); + } + }) + ); + logInfo("Step 6: Event listeners registered."); + + // NEW: Clear diagnostics when a document is closed + context.subscriptions.push( + vscode.workspace.onDidCloseTextDocument(doc => errorDiagnostics.delete(doc.uri)) + ); + + // Initial check for the currently active file, if any + logInfo("Step 7: Performing initial check for active editor..."); + if (vscode.window.activeTextEditor) { + debouncedErrorCheck(vscode.window.activeTextEditor.document); + } + logInfo("Step 7: Initial check complete."); // Register configuration change listener context.subscriptions.push( diff --git a/utils/checkErrors.js b/utils/checkErrors.js new file mode 100644 index 0000000..7b22103 --- /dev/null +++ b/utils/checkErrors.js @@ -0,0 +1,19 @@ +const api = require("../api/api") + +/** + * Sends Perl code to the backend to be checked for errors. + * This function uses the pre-configured 'api' instance. + * @param {string} code - The Perl code string to analyze. + * @returns {Promise} The full response from the API. The actual data + * will be in the .data property of the response, + * which is expected to contain an 'errors' array. + */ +const checkCodeForErrors = (code) => { + // 2. Use the 'api' instance to make the POST request. + // The endpoint '/checkErrors/' will be appended to the baseURL. + return api.post('/checkErrors/', { code }); +}; + +// 3. Export both the 'api' instance for general use and the specific +// 'checkCodeForErrors' function for its dedicated task. +module.exports =checkCodeForErrors; \ No newline at end of file From 9d40ce5de8d46d12f78703d4665fd9536df64ad1 Mon Sep 17 00:00:00 2001 From: "R.Sinthujan" Date: Fri, 18 Jul 2025 19:21:23 +0530 Subject: [PATCH 2/2] minor issues resolved --- extension.js | 246 +++++++++++++++++++++++-------------------- utils/checkErrors.js | 16 +-- 2 files changed, 141 insertions(+), 121 deletions(-) diff --git a/extension.js b/extension.js index 8f0cc0a..657912d 100644 --- a/extension.js +++ b/extension.js @@ -44,8 +44,11 @@ function createRequestKey(comment, doc, pos) { let codebaseIndexer = null; let outputChannel = null; let debounceTime = 3000; +let errorCheckDebounceTime = 2000; // NEW: Diagnostic collection for displaying errors let errorDiagnostics = null; +let errorCheckAbortController = new AbortController(); + // /** // * Fetches code suggestion based on a comment // * @param {string} comment - The user's comment @@ -101,68 +104,69 @@ async function fetchCodeWithDeduplication(comment, doc, pos) { /** - * NEW: Analyzes the entire document for errors and updates diagnostics - * @param {vscode.TextDocument} doc - The document to check + * Analyzes the document for errors and underlines the entire line. + * @param {vscode.TextDocument} doc - The document to check. */ async function updateErrorDiagnostics(doc) { - if (doc.languageId !== 'perl') { - return; // Only check Perl files - } + if (doc.languageId !== 'perl') return; - logInfo(`Running error check for: ${doc.fileName}`); - try { - const code = doc.getText(); - const lines = code.split('\n'); - - // Create array of lines with their line numbers, including empty lines - // Create formatted string with padded line numbers - const totalLines = lines.length; - const padding = totalLines.toString().length; - const linesWithNumbers = lines - .map((text, index) => { - const lineNumber = (index + 1).toString().padStart(padding, ' '); - return `${lineNumber}: ${text}`; - }) - .join('\n'); - + // Cancel any previous, still-running check to prevent "ghost errors" + errorCheckAbortController.abort(); + errorCheckAbortController = new AbortController(); + const signal = errorCheckAbortController.signal; - - // Send code with line numbers to backend - const response = await checkCodeForErrors(linesWithNumbers); - const errors = response.data.errors; // Assuming the errors are in response.data.errors + logInfo(`Running error check for: ${doc.fileName}`); + try { + const code = doc.getText(); + const response = await checkCodeForErrors(code, signal); + const errors = response.data.errors; - if (!Array.isArray(errors)) { - logError("Received invalid error format from API.", errors); - return; - } + if (!Array.isArray(errors)) { + logError("Received invalid error format from API.", errors); + return; + } - const diagnostics = errors.map(error => { - // VS Code lines are 0-indexed, API might return 1-indexed - const line = Math.max(0, error.line - 1); - const startChar = error.start || 0; - const endChar = error.end || Math.max(startChar + 1, lines[line]?.length || 0); - - const range = new vscode.Range( - new vscode.Position(line, startChar), - new vscode.Position(line, endChar) - ); - - const diagnostic = new vscode.Diagnostic(range, error.message, vscode.DiagnosticSeverity.Error); - diagnostic.source = 'Perl AI Assistant'; - return diagnostic; - }); + // --- NEW LOGIC: Underline the entire line --- + const diagnostics = errors.map(error => { + // The API now returns only line and message. + const { line: errorLine, message } = error; + + // Convert 1-based line from AI to 0-based for VS Code. + const lineIndex = Math.max(0, errorLine - 1); + + if (lineIndex >= doc.lineCount) { + logError(`API returned invalid line number: ${errorLine}`); + return null; // Skip this error if the line doesn't exist + } - errorDiagnostics.set(doc.uri, diagnostics); - logInfo(`Found ${diagnostics.length} errors.`); + const lineText = doc.lineAt(lineIndex); + // Create a range that covers the entire line, from the first character to the last. + const range = new vscode.Range( + new vscode.Position(lineIndex, 0), + new vscode.Position(lineIndex, lineText.text.length) + ); + + const diagnostic = new vscode.Diagnostic(range, message, vscode.DiagnosticSeverity.Error); + diagnostic.source = 'Perl AI Assistant'; + return diagnostic; + }).filter(diag => diag !== null); // Filter out any null diagnostics - } catch (err) { - logInfo(`Failed to check for errors: ${err.message}`, err); - // Do not show an error message to the user to avoid being disruptive + errorDiagnostics.set(doc.uri, diagnostics); + logInfo(`Found ${diagnostics.length} errors.`); + + } catch (err) { + // If the error was due to cancellation, it's expected, so we just log it quietly. + if (err.name === 'CanceledError' || err.name === 'AbortError') { + logInfo('Error check was cancelled because a new one was started.'); + } else { + logInfo(`Failed to check for errors: ${err.message}`); } + } } + /** * Collects and generates context for AI code generation * @param {string} comment - The user's comment @@ -344,21 +348,14 @@ async function activate(context) { // Create debounced version of fetchCode const debouncedFetch = debounce(fetchCodeWithDeduplication, debounceTime); - // NEW: Create a debounced version of the error checker const debouncedErrorCheck = debounce( (doc) => updateErrorDiagnostics(doc), - config.errorCheckDebounceTime + errorCheckDebounceTime ); - logInfo("Step 4: Debounced functions created."); - // NEW: Initialize Diagnostics Collection - logInfo("Step 5: Initializing diagnostics collection..."); errorDiagnostics = vscode.languages.createDiagnosticCollection("perl-ai-errors"); context.subscriptions.push(errorDiagnostics); - logInfo("Step 5: Diagnostics collection initialized."); - // NEW: Register event listener for when a text document is changed - logInfo("Step 6: Registering event listeners..."); context.subscriptions.push( vscode.workspace.onDidChangeTextDocument(event => { if (vscode.window.activeTextEditor && event.document === vscode.window.activeTextEditor.document) { @@ -367,38 +364,32 @@ async function activate(context) { }) ); - // NEW: Register event listener for when the active editor changes context.subscriptions.push( vscode.window.onDidChangeActiveTextEditor(editor => { if (editor) { - // Trigger an immediate check when switching to a new file debouncedErrorCheck(editor.document); } }) ); - logInfo("Step 6: Event listeners registered."); - // NEW: Clear diagnostics when a document is closed context.subscriptions.push( vscode.workspace.onDidCloseTextDocument(doc => errorDiagnostics.delete(doc.uri)) ); - // Initial check for the currently active file, if any - logInfo("Step 7: Performing initial check for active editor..."); if (vscode.window.activeTextEditor) { debouncedErrorCheck(vscode.window.activeTextEditor.document); } - logInfo("Step 7: Initial check complete."); context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('perlCodeGeneration')) { + if (e.affectsConfiguration('perlCodeGeneration')) { // FIX: Corrected typo 'perlCodegeneration' loadConfiguration(); logInfo("Configuration updated", config); } }) ); + const inlineCompletionProvider = { async provideInlineCompletionItems(doc, pos) { const line = doc.lineAt(pos).text; @@ -438,61 +429,90 @@ async function activate(context) { logInfo("Extension setup complete"); - const processSelectionForSidebar = debounce(async (event) => { - const selection = event.selections[0]; - const doc = event.textEditor.document; - - // Handle non-Perl files first - if (doc.languageId !== 'perl') { - logInfo(`Skipping suggestion for non-Perl file: ${doc.languageId}`); - // Display an error message if it's not a Perl file - treeProvider.refresh([`Error: Only Perl code suggestions are supported. Current file is a '${doc.languageId}' file.`]); - return; - } - - if (!selection || selection.isEmpty) { - treeProvider.refresh([]); // Clear sidebar if selection is empty in a Perl file - return; - } - - const selectedText = doc.getText(selection); +// Add these variables at the top with other global state +let lastSelectedText = ''; +let lastSuggestions = []; + +// Replace the processSelectionForSidebar function with this improved version +const processSelectionForSidebar = debounce(async (event) => { + const selection = event.selections[0]; + const doc = event.textEditor.document; + + // Enhanced Perl file detection - check both languageId and file extension + const isPerlFile = doc.languageId === 'perl' || + doc.fileName.endsWith('.pl') || + doc.fileName.endsWith('.pm') || + doc.fileName.endsWith('.t'); + + // Handle non-Perl files first + if (!isPerlFile) { + logInfo(`Skipping suggestion for non-Perl file: ${doc.languageId}, filename: ${doc.fileName}`); + treeProvider.refresh([`Error: Only Perl code suggestions are supported. Current file is a '${doc.languageId}' file (${doc.fileName}).`]); + return; + } + + if (!selection || selection.isEmpty) { + // Don't clear immediately - only clear if we had no previous selection + if (lastSelectedText === '') { + treeProvider.refresh([]); + } + return; + } - if (selectedText.trim()) { - try { - logInfo("Sending request to backend for alternative suggestions..."); - - const response = await api.post('/altCode/', { code: selectedText }); - - const alternatives = response.data.alternatives || []; - - const suggestionsForSidebar = alternatives.map(item => item.code); + const selectedText = doc.getText(selection); + + // If the selected text is the same as last time, don't make a new request + if (selectedText.trim() === lastSelectedText.trim() && lastSuggestions.length > 0) { + logInfo("Using cached suggestions for same selection"); + treeProvider.refresh(lastSuggestions); + return; + } - if (suggestionsForSidebar.length === 0) { - suggestionsForSidebar.push("No specific code suggestions received from AI, or response format was unexpected."); - } + if (selectedText.trim()) { + try { + logInfo("Sending request to backend for alternative suggestions..."); + + const response = await api.post('/altCode/', { code: selectedText }); + + const alternatives = response.data.alternatives || []; + + const suggestionsForSidebar = alternatives.map(item => item.code); - treeProvider.refresh(suggestionsForSidebar); - logInfo("Sidebar refreshed with backend suggestions."); + if (suggestionsForSidebar.length === 0) { + suggestionsForSidebar.push("No specific code suggestions received from AI, or response format was unexpected."); + } - } catch (err) { - logError('Failed to fetch alternative suggestions from backend', err); - - let userFacingErrorMessage = "An unexpected error occurred. Please check the Debug Console for details."; - - if (err.code === 'ECONNREFUSED') { - userFacingErrorMessage = "Error: Backend server is not running. Please start your FastAPI backend."; - } else if (err.response && err.response.data && err.response.data.alternatives && err.response.data.alternatives.length > 0) { - userFacingErrorMessage = `Backend Error: ${err.response.data.alternatives[0].code}`; - } else if (err.message) { - userFacingErrorMessage = `Error fetching suggestions: ${err.message}`; - } + // Cache the results + lastSelectedText = selectedText.trim(); + lastSuggestions = suggestionsForSidebar; + + treeProvider.refresh(suggestionsForSidebar); + logInfo("Sidebar refreshed with backend suggestions."); - treeProvider.refresh([userFacingErrorMessage]); + } catch (err) { + logError('Failed to fetch alternative suggestions from backend', err); + + let userFacingErrorMessage = "An unexpected error occurred. Please check the Debug Console for details."; + + if (err.code === 'ECONNREFUSED') { + userFacingErrorMessage = "Error: Backend server is not running. Please start your FastAPI backend."; + } else if (err.response && err.response.data && err.response.data.alternatives && err.response.data.alternatives.length > 0) { + userFacingErrorMessage = `Backend Error: ${err.response.data.alternatives[0].code}`; + } else if (err.message) { + userFacingErrorMessage = `Error fetching suggestions: ${err.message}`; } - } else { - treeProvider.refresh([]); + + treeProvider.refresh([userFacingErrorMessage]); } - }, config.debounceTime); + } else { + // Only clear if we're moving away from a selection + if (lastSelectedText !== '') { + lastSelectedText = ''; + lastSuggestions = []; + treeProvider.refresh([]); + } + } +}, debounceTime); context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection(processSelectionForSidebar) ); diff --git a/utils/checkErrors.js b/utils/checkErrors.js index 7b22103..c987bd1 100644 --- a/utils/checkErrors.js +++ b/utils/checkErrors.js @@ -2,18 +2,18 @@ const api = require("../api/api") /** * Sends Perl code to the backend to be checked for errors. - * This function uses the pre-configured 'api' instance. + * This function now accepts an AbortSignal to allow for request cancellation. * @param {string} code - The Perl code string to analyze. - * @returns {Promise} The full response from the API. The actual data - * will be in the .data property of the response, - * which is expected to contain an 'errors' array. + * @param {AbortSignal} signal - The signal to cancel the request. + * @returns {Promise} The full response from the API. */ -const checkCodeForErrors = (code) => { - // 2. Use the 'api' instance to make the POST request. - // The endpoint '/checkErrors/' will be appended to the baseURL. - return api.post('/checkErrors/', { code }); +const checkCodeForErrors = (code, signal) => { + // Use the correct endpoint and pass the signal. + // If the signal is aborted elsewhere, Axios will cancel this request. + return api.post('/checkErrors/', { code }, { signal }); }; + // 3. Export both the 'api' instance for general use and the specific // 'checkCodeForErrors' function for its dedicated task. module.exports =checkCodeForErrors; \ No newline at end of file