Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const axios = require("axios");
module.exports = axios.create({
baseURL: "http://127.0.0.1:8000",
});
});


313 changes: 250 additions & 63 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,164 @@ 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')
const { AlternativeSuggestionsProvider } = require('./sidebarProvider');
/**
* Global extension configuration
*/
const config = {
debounceTime: 500,
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,
};

// 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<string>} Generated code suggestion
* @returns {string} Unique key for the request
*/
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;
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
// * @param {vscode.TextDocument} doc - Current document
// * @param {vscode.Position} pos - Current cursor position
// * @returns {Promise<string>} 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;
}


/**
* Analyzes the document for errors and underlines the entire line.
* @param {vscode.TextDocument} doc - The document to check.
*/
async function fetchCode(comment, doc, pos) {
async function updateErrorDiagnostics(doc) {
if (doc.languageId !== 'perl') return;

// Cancel any previous, still-running check to prevent "ghost errors"
errorCheckAbortController.abort();
errorCheckAbortController = new AbortController();
const signal = errorCheckAbortController.signal;

logInfo(`Running error check for: ${doc.fileName}`);
try {
const ctx = await generateContextForComments(comment, doc, pos);
const response = await api.post('/commentCode/', { message: comment, context: ctx });
return response.data.code;
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;
}

// --- 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
}

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

errorDiagnostics.set(doc.uri, diagnostics);
logInfo(`Found ${diagnostics.length} errors.`);

} catch (err) {
logError(`Error fetching suggestion: ${err.message}`, err);
vscode.window.showErrorMessage(`Failed to generate code: ${err.message}`);
return null;
// 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
Expand Down Expand Up @@ -221,17 +344,52 @@ async function activate(context) {
});
}, 300);

const debouncedFetch = debounce(fetchCode, config.debounceTime);

// Create debounced version of fetchCode
const debouncedFetch = debounce(fetchCodeWithDeduplication, debounceTime);

const debouncedErrorCheck = debounce(
(doc) => updateErrorDiagnostics(doc),
errorCheckDebounceTime
);

errorDiagnostics = vscode.languages.createDiagnosticCollection("perl-ai-errors");
context.subscriptions.push(errorDiagnostics);

context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument(event => {
if (vscode.window.activeTextEditor && event.document === vscode.window.activeTextEditor.document) {
debouncedErrorCheck(event.document);
}
})
);

context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(editor => {
if (editor) {
debouncedErrorCheck(editor.document);
}
})
);

context.subscriptions.push(
vscode.workspace.onDidCloseTextDocument(doc => errorDiagnostics.delete(doc.uri))
);

if (vscode.window.activeTextEditor) {
debouncedErrorCheck(vscode.window.activeTextEditor.document);
}

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;
Expand Down Expand Up @@ -271,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)
);
Expand Down
Loading