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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@
"type": "object",
"properties": {
"filePaths": {
"description": "The absolute paths to the files to check for errors. Omit 'filePaths' when retrieving all errors.",
"description": "The absolute paths to the files or folders to check for errors. Omit 'filePaths' when retrieving all errors.",
"type": "array",
"items": {
"type": "string"
Expand Down
109 changes: 85 additions & 24 deletions src/extension/tools/node/getErrorsTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,26 @@ import { BasePromptElementProps, PromptElement, PromptElementProps } from '@vsco
import type * as vscode from 'vscode';
import { ILanguageDiagnosticsService } from '../../../platform/languages/common/languageDiagnosticsService';
import { ILogService } from '../../../platform/log/common/logService';
import { INotebookService } from '../../../platform/notebook/common/notebookService';
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
import { getLanguage } from '../../../util/common/languages';
import { findNotebook } from '../../../util/common/notebooks';
import { isLocation } from '../../../util/common/types';
import { coalesce } from '../../../util/vs/base/common/arrays';
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { isEqualOrParent } from '../../../util/vs/base/common/resources';
import { URI } from '../../../util/vs/base/common/uri';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { DiagnosticSeverity, ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString, Range } from '../../../vscodeTypes';
import { findDiagnosticForSelectionAndPrompt } from '../../context/node/resolvers/fixSelection';
import { IBuildPromptContext } from '../../prompt/common/intents';
import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer';
import { Tag } from '../../prompts/node/base/tag';
import { DiagnosticContext, Diagnostics } from '../../prompts/node/inline/diagnosticsContext';
import { ToolName } from '../common/toolNames';
import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry';
import { checkCancellation, formatUriForFileWidget, resolveToolInputPath } from './toolUtils';
import { INotebookService } from '../../../platform/notebook/common/notebookService';
import { findNotebook } from '../../../util/common/notebooks';

interface IGetErrorsParams {
// Note that empty array is not the same as absence; empty array
Expand All @@ -38,7 +38,7 @@ interface IGetErrorsParams {
ranges?: ([a: number, b: number, c: number, d: number] | undefined)[];
}

class GetErrorsTool extends Disposable implements ICopilotTool<IGetErrorsParams> {
export class GetErrorsTool extends Disposable implements ICopilotTool<IGetErrorsParams> {
public static readonly toolName = ToolName.GetErrors;

constructor(
Expand All @@ -52,33 +52,94 @@ class GetErrorsTool extends Disposable implements ICopilotTool<IGetErrorsParams>
super();
}

/**
* Get diagnostics for the given paths and optional ranges.
* Note - This is made public for testing purposes only.
*/
public getDiagnostics(paths: { uri: URI; range: Range | undefined }[]): Array<{ uri: URI; diagnostics: vscode.Diagnostic[] }> {
const results: Array<{ uri: URI; diagnostics: vscode.Diagnostic[] }> = [];

// for notebooks, we need to find the cell matching the range and get diagnostics for that cell
const nonNotebookPaths = paths.filter(p => {
const isNotebook = this.notebookService.hasSupportedNotebooks(p.uri);
if (isNotebook) {
const diagnostics = this.getNotebookCellDiagnostics(p.uri);
results.push({ uri: p.uri, diagnostics });
}

return !isNotebook;
});

if (nonNotebookPaths.length === 0) {
return results;
}

const pendingMatchPaths = new Set(nonNotebookPaths.map(p => p.uri));

// for non-notebooks, we get all diagnostics and filter down
for (const [resource, entries] of this.languageDiagnosticsService.getAllDiagnostics()) {
const pendingDiagnostics = entries.filter(d => d.severity <= DiagnosticSeverity.Warning);

// find all path&range pairs and collect the ranges to further filter diagnostics
// if any path matches the resource without a range, take all diagnostics for that file
// otherwise, filter diagnostics to those intersecting one of the provided ranges
const ranges: Range[] = [];
let shouldTakeAll = false;
let foundMatch = false;
for (const path of nonNotebookPaths) {
// we support file or folder paths
if (isEqualOrParent(resource, path.uri)) {
foundMatch = true;

if (pendingMatchPaths.has(path.uri)) {
pendingMatchPaths.delete(path.uri);
}

if (path.range) {
ranges.push(path.range);
} else {
// no range, so all diagnostics for this file
shouldTakeAll = true;
break;
}
}
}

if (shouldTakeAll) {
results.push({ uri: resource, diagnostics: pendingDiagnostics });
continue;
}

if (foundMatch && ranges.length > 0) {
const diagnostics = pendingDiagnostics.filter(d => ranges.some(range => d.range.intersection(range)));
results.push({ uri: resource, diagnostics });
}
}

// for any given paths that didn't match any files, return empty diagnostics for each of them
for (const uri of pendingMatchPaths) {
results.push({ uri, diagnostics: [] });
}

return results;
}

async invoke(options: vscode.LanguageModelToolInvocationOptions<IGetErrorsParams>, token: CancellationToken) {
const getAll = () => this.languageDiagnosticsService.getAllDiagnostics()
.map(d => ({ uri: d[0], diagnostics: d[1].filter(e => e.severity <= DiagnosticSeverity.Warning) }))
// filter any documents w/o warnings or errors
.filter(d => d.diagnostics.length > 0);

const getSome = (filePaths: string[]) => filePaths.map((filePath, i) => {
const uri = resolveToolInputPath(filePath, this.promptPathRepresentationService);
const range = options.input.ranges?.[i];
if (!uri) {
throw new Error(`Invalid input path ${filePath}`);
}

let diagnostics: vscode.Diagnostic[] = [];
if (this.notebookService.hasSupportedNotebooks(uri)) {
diagnostics = this.getNotebookCellDiagnostics(uri);
} else {
diagnostics = range
? findDiagnosticForSelectionAndPrompt(this.languageDiagnosticsService, uri, new Range(...range), undefined)
: this.languageDiagnosticsService.getDiagnostics(uri);
}
const getSome = (filePaths: string[]) =>
this.getDiagnostics(filePaths.map((filePath, i) => {
const uri = resolveToolInputPath(filePath, this.promptPathRepresentationService);
const range = options.input.ranges?.[i];
if (!uri) {
throw new Error(`Invalid input path ${filePath}`);
}

return {
diagnostics: diagnostics.filter(d => d.severity <= DiagnosticSeverity.Warning),
uri,
};
});
return { uri, range: range ? new Range(...range) : undefined };
}));

const ds = options.input.filePaths?.length ? getSome(options.input.filePaths) : getAll();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`GetErrorsResult > diagnostics with max 1`] = `
"Showing first 1 results out of 2
<errors path="/test/workspace/file.ts">
This code at line 1
\`\`\`
line 1
\`\`\`
has the problem reported:
<compileError>
error
</compileError>

</errors>
"
`;

exports[`GetErrorsResult > diagnostics with more complex max 1`] = `
"Showing first 3 results out of 4
<errors path="/test/workspace/file.ts">
This code at line 1
\`\`\`
line 1
\`\`\`
has the problem reported:
<compileError>
error
</compileError>
This code at line 2
\`\`\`
line 2
\`\`\`
has the problem reported:
<compileError>
error 2
</compileError>

</errors>
<errors path="/test/workspace/file2.ts">
This code at line 1
\`\`\`
line 1
\`\`\`
has the problem reported:
<compileError>
error
</compileError>

</errors>
"
`;

exports[`GetErrorsResult > simple diagnostics 1`] = `
"<errors path="/test/workspace/file.ts">
This code at line 1
\`\`\`
line 1
\`\`\`
has the problem reported:
<compileError>
error
</compileError>
This code at line 2
\`\`\`
line 2
\`\`\`
has the problem reported:
<compileError>
error 2
</compileError>

</errors>
"
`;

exports[`GetErrorsTool > diagnostics with max 1`] = `
"Showing first 1 results out of 2
<errors path="/test/workspace/file.ts">
This code at line 1
\`\`\`
line 1
\`\`\`
has the problem reported:
<compileError>
error
</compileError>

</errors>
"
`;

exports[`GetErrorsTool > diagnostics with more complex max 1`] = `
"Showing first 3 results out of 4
<errors path="/test/workspace/file.ts">
This code at line 1
\`\`\`
line 1
\`\`\`
has the problem reported:
<compileError>
error
</compileError>
This code at line 2
\`\`\`
line 2
\`\`\`
has the problem reported:
<compileError>
error 2
</compileError>

</errors>
<errors path="/test/workspace/file2.ts">
This code at line 1
\`\`\`
line 1
\`\`\`
has the problem reported:
<compileError>
error
</compileError>

</errors>
"
`;

exports[`GetErrorsTool > simple diagnostics 1`] = `
"<errors path="/test/workspace/file.ts">
This code at line 1
\`\`\`
line 1
\`\`\`
has the problem reported:
<compileError>
error
</compileError>
This code at line 2
\`\`\`
line 2
\`\`\`
has the problem reported:
<compileError>
error 2
</compileError>

</errors>
"
`;
Loading
Loading