Refactor: Change renderLocation in webview
* It is now more general and the logic is simplified * Also, add more comments * Rename `adaptBqrs` to `transformBqrsResultSet` * Remove a react error for missing a key attribute in a list
This commit is contained in:
@@ -90,7 +90,13 @@ export interface RawResultSet {
|
|||||||
readonly rows: readonly ResultRow[];
|
readonly rows: readonly ResultRow[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adaptBqrs(schema: ResultSetSchema, page: DecodedBqrsChunk): RawResultSet {
|
// TODO: This function is not necessary. It generates a tuple that is slightly easier
|
||||||
|
// to handle than the ResultSetSchema and DecodedBqrsChunk. But perhaps it is unnecessary
|
||||||
|
// boilerplate.
|
||||||
|
export function transformBqrsResultSet(
|
||||||
|
schema: ResultSetSchema,
|
||||||
|
page: DecodedBqrsChunk
|
||||||
|
): RawResultSet {
|
||||||
return {
|
return {
|
||||||
schema,
|
schema,
|
||||||
rows: Array.from(page.tuples),
|
rows: Array.from(page.tuples),
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ function isWholeFileMatch(matches: RegExpExecArray): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the file path is empty. For now, just check whether
|
* Checks whether the file path is empty. If so, we do not want to render this location
|
||||||
* the file path is empty. If so, we do not want to render this location
|
|
||||||
* as a link.
|
* as a link.
|
||||||
*
|
*
|
||||||
* @param uri A file uri
|
* @param uri A file uri
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Logger } from '../logging';
|
|||||||
import { CodeQLCliServer } from '../cli';
|
import { CodeQLCliServer } from '../cli';
|
||||||
import { DatabaseManager } from '../databases';
|
import { DatabaseManager } from '../databases';
|
||||||
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
|
import { getHtmlForWebview, jumpToLocation } from '../interface-utils';
|
||||||
import { adaptBqrs, RawResultSet, BQRSInfo } from '../bqrs-cli-types';
|
import { transformBqrsResultSet, RawResultSet, BQRSInfo } from '../bqrs-cli-types';
|
||||||
import resultsDiff from './resultsDiff';
|
import resultsDiff from './resultsDiff';
|
||||||
|
|
||||||
interface ComparePair {
|
interface ComparePair {
|
||||||
@@ -256,7 +256,7 @@ export class CompareInterfaceManager extends DisposableObject {
|
|||||||
resultsPath,
|
resultsPath,
|
||||||
resultSetName
|
resultSetName
|
||||||
);
|
);
|
||||||
return adaptBqrs(schema, chunk);
|
return transformBqrsResultSet(schema, chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
private compareResults(
|
private compareResults(
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import {
|
|||||||
jumpToLocation,
|
jumpToLocation,
|
||||||
} from './interface-utils';
|
} from './interface-utils';
|
||||||
import { getDefaultResultSetName, ParsedResultSets } from './interface-types';
|
import { getDefaultResultSetName, ParsedResultSets } from './interface-types';
|
||||||
import { RawResultSet, adaptBqrs, ResultSetSchema } from './bqrs-cli-types';
|
import { RawResultSet, transformBqrsResultSet, ResultSetSchema } from './bqrs-cli-types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* interface.ts
|
* interface.ts
|
||||||
@@ -372,7 +372,7 @@ export class InterfaceManager extends DisposableObject {
|
|||||||
pageSize: RAW_RESULTS_PAGE_SIZE
|
pageSize: RAW_RESULTS_PAGE_SIZE
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const resultSet = adaptBqrs(schema, chunk);
|
const resultSet = transformBqrsResultSet(schema, chunk);
|
||||||
return {
|
return {
|
||||||
pageNumber: 0,
|
pageNumber: 0,
|
||||||
numPages: numPagesOfResultSet(resultSet),
|
numPages: numPagesOfResultSet(resultSet),
|
||||||
@@ -485,7 +485,7 @@ export class InterfaceManager extends DisposableObject {
|
|||||||
pageSize: RAW_RESULTS_PAGE_SIZE
|
pageSize: RAW_RESULTS_PAGE_SIZE
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const resultSet = adaptBqrs(schema, chunk);
|
const resultSet = transformBqrsResultSet(schema, chunk);
|
||||||
|
|
||||||
const parsedResultSets: ParsedResultSets = {
|
const parsedResultSets: ParsedResultSets = {
|
||||||
pageNumber,
|
pageNumber,
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ export interface SarifLink {
|
|||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The type of a result that has no associated location.
|
||||||
|
// hint is a string intended for display to the user
|
||||||
|
// that explains why there is no location.
|
||||||
interface NoLocation {
|
interface NoLocation {
|
||||||
hint: string;
|
hint: string;
|
||||||
}
|
}
|
||||||
@@ -85,7 +88,6 @@ export function parseSarifLocation(
|
|||||||
// file uri or a relative uri.
|
// file uri or a relative uri.
|
||||||
const uri = physicalLocation.artifactLocation.uri;
|
const uri = physicalLocation.artifactLocation.uri;
|
||||||
|
|
||||||
// FIXME: This is probably wrong
|
|
||||||
const fileUriRegex = /^file:/;
|
const fileUriRegex = /^file:/;
|
||||||
const effectiveLocation = uri.match(fileUriRegex)
|
const effectiveLocation = uri.match(fileUriRegex)
|
||||||
? uri
|
? uri
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import * as React from 'react';
|
|||||||
|
|
||||||
import { renderLocation } from './result-table-utils';
|
import { renderLocation } from './result-table-utils';
|
||||||
import { ColumnValue } from '../bqrs-cli-types';
|
import { ColumnValue } from '../bqrs-cli-types';
|
||||||
import { isStringLoc, isWholeFileLoc, isLineColumnLoc } from '../bqrs-utils';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: ColumnValue;
|
value: ColumnValue;
|
||||||
@@ -11,20 +10,13 @@ interface Props {
|
|||||||
|
|
||||||
export default function RawTableValue(props: Props): JSX.Element {
|
export default function RawTableValue(props: Props): JSX.Element {
|
||||||
const v = props.value;
|
const v = props.value;
|
||||||
if (typeof v === 'string'
|
if (
|
||||||
|
typeof v === 'string'
|
||||||
|| typeof v === 'number'
|
|| typeof v === 'number'
|
||||||
|| typeof v === 'boolean') {
|
|| typeof v === 'boolean'
|
||||||
|
) {
|
||||||
return <span>{v}</span>;
|
return <span>{v}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loc = v.url;
|
return renderLocation(v.url, v.label, props.databaseUri);
|
||||||
if (!loc) {
|
|
||||||
return <span />;
|
|
||||||
} else if (isStringLoc(loc)) {
|
|
||||||
return <a href={loc}>{loc}</a>;
|
|
||||||
} else if (isWholeFileLoc(loc) || isLineColumnLoc(loc)) {
|
|
||||||
return renderLocation(loc, v.label, props.databaseUri);
|
|
||||||
} else {
|
|
||||||
return <span />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,13 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
|||||||
if ('hint' in parsedLoc) {
|
if ('hint' in parsedLoc) {
|
||||||
return renderNonLocation(text, parsedLoc.hint);
|
return renderNonLocation(text, parsedLoc.hint);
|
||||||
} else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) {
|
} else if (isWholeFileLoc(parsedLoc) || isLineColumnLoc(parsedLoc)) {
|
||||||
return renderLocation(parsedLoc, text, databaseUri, undefined, updateSelectionCallback(pathNodeKey));
|
return renderLocation(
|
||||||
|
parsedLoc,
|
||||||
|
text,
|
||||||
|
databaseUri,
|
||||||
|
undefined,
|
||||||
|
updateSelectionCallback(pathNodeKey)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -142,18 +148,33 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
|||||||
* Render sarif location as a link with the text being simply a
|
* Render sarif location as a link with the text being simply a
|
||||||
* human-readable form of the location itself.
|
* human-readable form of the location itself.
|
||||||
*/
|
*/
|
||||||
function renderSarifLocation(loc: Sarif.Location, pathNodeKey: Keys.PathNode | undefined): JSX.Element | undefined {
|
function renderSarifLocation(
|
||||||
|
loc: Sarif.Location,
|
||||||
|
pathNodeKey: Keys.PathNode | undefined
|
||||||
|
): JSX.Element | undefined {
|
||||||
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
|
const parsedLoc = parseSarifLocation(loc, sourceLocationPrefix);
|
||||||
if ('hint' in parsedLoc) {
|
if ('hint' in parsedLoc) {
|
||||||
return renderNonLocation('[no location]', parsedLoc.hint);
|
return renderNonLocation('[no location]', parsedLoc.hint);
|
||||||
} else if (isWholeFileLoc(parsedLoc)) {
|
} else if (isWholeFileLoc(parsedLoc)) {
|
||||||
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
|
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}`;
|
||||||
const longLocation = `${parsedLoc.userVisibleFile}`;
|
const longLocation = `${parsedLoc.userVisibleFile}`;
|
||||||
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
|
return renderLocation(
|
||||||
|
parsedLoc,
|
||||||
|
shortLocation,
|
||||||
|
databaseUri,
|
||||||
|
longLocation,
|
||||||
|
updateSelectionCallback(pathNodeKey)
|
||||||
|
);
|
||||||
} else if (isLineColumnLoc(parsedLoc)) {
|
} else if (isLineColumnLoc(parsedLoc)) {
|
||||||
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.startLine}:${parsedLoc.startColumn}`;
|
const shortLocation = `${path.basename(parsedLoc.userVisibleFile)}:${parsedLoc.startLine}:${parsedLoc.startColumn}`;
|
||||||
const longLocation = `${parsedLoc.userVisibleFile}`;
|
const longLocation = `${parsedLoc.userVisibleFile}`;
|
||||||
return renderLocation(parsedLoc, shortLocation, databaseUri, longLocation, updateSelectionCallback(pathNodeKey));
|
return renderLocation(
|
||||||
|
parsedLoc,
|
||||||
|
shortLocation,
|
||||||
|
databaseUri,
|
||||||
|
longLocation,
|
||||||
|
updateSelectionCallback(pathNodeKey)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -163,9 +184,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
|||||||
return (e) => this.toggle(e, indices);
|
return (e) => this.toggle(e, indices);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (resultSet.sarif.runs.length === 0 ||
|
if (!resultSet.sarif.runs?.[0]?.results?.length) {
|
||||||
resultSet.sarif.runs[0].results === undefined ||
|
|
||||||
resultSet.sarif.runs[0].results.length === 0) {
|
|
||||||
return this.renderNoResults();
|
return this.renderNoResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +221,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
|||||||
[expansionIndex];
|
[expansionIndex];
|
||||||
|
|
||||||
rows.push(
|
rows.push(
|
||||||
<tr {...zebraStripe(resultIndex)}>
|
<tr {...zebraStripe(resultIndex)} key={resultIndex}>
|
||||||
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}>
|
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler(indices)}>
|
||||||
{indicator}
|
{indicator}
|
||||||
</td>
|
</td>
|
||||||
@@ -223,7 +242,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
|||||||
if (currentResultExpanded) {
|
if (currentResultExpanded) {
|
||||||
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
|
const indicator = currentPathExpanded ? octicons.chevronDown : octicons.chevronRight;
|
||||||
rows.push(
|
rows.push(
|
||||||
<tr {...zebraStripe(resultIndex)}>
|
<tr {...zebraStripe(resultIndex)} key={`${resultIndex}-${pathIndex}`}>
|
||||||
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
|
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
|
||||||
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler([expansionIndex])}>{indicator}</td>
|
<td className="vscode-codeql__icon-cell vscode-codeql__dropdown-cell" onMouseDown={toggler([expansionIndex])}>{indicator}</td>
|
||||||
<td className="vscode-codeql__text-center" colSpan={3}>
|
<td className="vscode-codeql__text-center" colSpan={3}>
|
||||||
@@ -249,7 +268,7 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
|||||||
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
|
const stepIndex = pathNodeIndex + 1; // Convert to 1-based
|
||||||
const zebraIndex = resultIndex + stepIndex;
|
const zebraIndex = resultIndex + stepIndex;
|
||||||
rows.push(
|
rows.push(
|
||||||
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined}>
|
<tr className={isSelected ? 'vscode-codeql__selected-path-node' : undefined} key={`${resultIndex}-${pathIndex}-${pathNodeIndex}`}>
|
||||||
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
|
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
|
||||||
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
|
<td className="vscode-codeql__icon-cell"><span className="vscode-codeql__vertical-rule"></span></td>
|
||||||
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td>
|
<td {...selectableZebraStripe(isSelected, zebraIndex, 'vscode-codeql__path-index-cell')}>{stepIndex}</td>
|
||||||
@@ -264,9 +283,13 @@ export class PathTable extends React.Component<PathTableProps, PathTableState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (numTruncatedResults > 0) {
|
if (numTruncatedResults > 0) {
|
||||||
rows.push(<tr><td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}>
|
rows.push(
|
||||||
Too many results to show at once. {numTruncatedResults} result(s) omitted.
|
<tr key="truncatd-message">
|
||||||
</td></tr>);
|
<td colSpan={5} style={{ textAlign: 'center', fontStyle: 'italic' }}>
|
||||||
|
Too many results to show at once. {numTruncatedResults} result(s) omitted.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <table className={className}>
|
return <table className={className}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { UrlValue, ResolvableLocationValue } from '../bqrs-cli-types';
|
import { UrlValue, ResolvableLocationValue } from '../bqrs-cli-types';
|
||||||
import { tryGetResolvableLocation } from '../bqrs-utils';
|
import { isStringLoc, tryGetResolvableLocation } from '../bqrs-utils';
|
||||||
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
|
import { RawResultsSortState, QueryMetadata, SortDirection } from '../interface-types';
|
||||||
import { assertNever } from '../helpers-pure';
|
import { assertNever } from '../helpers-pure';
|
||||||
import { ResultSet } from '../interface-types';
|
import { ResultSet } from '../interface-types';
|
||||||
@@ -60,28 +60,41 @@ export function jumpToLocation(loc: ResolvableLocationValue, databaseUri: string
|
|||||||
/**
|
/**
|
||||||
* Render a location as a link which when clicked displays the original location.
|
* Render a location as a link which when clicked displays the original location.
|
||||||
*/
|
*/
|
||||||
export function renderLocation(loc: UrlValue | undefined, label: string | undefined,
|
export function renderLocation(
|
||||||
databaseUri: string, title?: string, callback?: () => void): JSX.Element {
|
loc: UrlValue | undefined,
|
||||||
|
label: string | undefined,
|
||||||
|
databaseUri: string,
|
||||||
|
title?: string,
|
||||||
|
callback?: () => void
|
||||||
|
): JSX.Element {
|
||||||
|
|
||||||
|
if (loc === undefined) {
|
||||||
|
return <span />;
|
||||||
|
} else if (isStringLoc(loc)) {
|
||||||
|
return <a href={loc}>{loc}</a>;
|
||||||
|
}
|
||||||
|
|
||||||
// If the label was empty, use a placeholder instead, so the link is still clickable.
|
// If the label was empty, use a placeholder instead, so the link is still clickable.
|
||||||
let displayLabel = label;
|
let displayLabel = label;
|
||||||
if (label === undefined || label === '')
|
if (!label) {
|
||||||
displayLabel = '[empty string]';
|
displayLabel = '[empty string]';
|
||||||
else if (label.match(/^\s+$/))
|
} else if (label.match(/^\s+$/)) {
|
||||||
displayLabel = `[whitespace: "${label}"]`;
|
displayLabel = `[whitespace: "${label}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
if (loc !== undefined) {
|
const resolvableLoc = tryGetResolvableLocation(loc);
|
||||||
const resolvableLoc = tryGetResolvableLocation(loc);
|
if (resolvableLoc !== undefined) {
|
||||||
if (resolvableLoc !== undefined) {
|
return (
|
||||||
return <a href="#"
|
<a href="#"
|
||||||
className="vscode-codeql__result-table-location-link"
|
className="vscode-codeql__result-table-location-link"
|
||||||
title={title}
|
title={title}
|
||||||
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>{displayLabel}</a>;
|
onClick={jumpToLocationHandler(resolvableLoc, databaseUri, callback)}>
|
||||||
} else {
|
{displayLabel}
|
||||||
return <span title={title}>{displayLabel}</span>;
|
</a>
|
||||||
}
|
);
|
||||||
|
} else {
|
||||||
|
return <span title={title}>{displayLabel}</span>;
|
||||||
}
|
}
|
||||||
return <span />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
QueryMetadata,
|
QueryMetadata,
|
||||||
ResultsPaths,
|
ResultsPaths,
|
||||||
ALERTS_TABLE_NAME,
|
ALERTS_TABLE_NAME,
|
||||||
ParsedResultSets,
|
ParsedResultSets
|
||||||
} from '../interface-types';
|
} from '../interface-types';
|
||||||
import { EventHandlers as EventHandlerList } from './event-handler-list';
|
import { EventHandlers as EventHandlerList } from './event-handler-list';
|
||||||
import { ResultTables } from './result-tables';
|
import { ResultTables } from './result-tables';
|
||||||
@@ -172,14 +172,17 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getResultSets(
|
private getResultSets(
|
||||||
resultsInfo: ResultsInfo
|
resultsInfo: ResultsInfo
|
||||||
): Promise<readonly ResultSet[]> {
|
): readonly ResultSet[] {
|
||||||
const parsedResultSets = resultsInfo.parsedResultSets;
|
const parsedResultSets = resultsInfo.parsedResultSets;
|
||||||
return [{
|
const resultSet = parsedResultSets.resultSet;
|
||||||
...parsedResultSets.resultSet,
|
if (!resultSet.t) {
|
||||||
t: (parsedResultSets.resultSet.t ?? 'RawResultSet') as any
|
throw new Error(
|
||||||
}];
|
'Missing result set type. Should be either "SarifResultSet" or "RawResultSet".'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [resultSet];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadResults(): Promise<void> {
|
private async loadResults(): Promise<void> {
|
||||||
@@ -191,7 +194,7 @@ class App extends React.Component<{}, ResultsViewState> {
|
|||||||
let results: Results | null = null;
|
let results: Results | null = null;
|
||||||
let statusText = '';
|
let statusText = '';
|
||||||
try {
|
try {
|
||||||
const resultSets = await this.getResultSets(resultsInfo);
|
const resultSets = this.getResultSets(resultsInfo);
|
||||||
results = {
|
results = {
|
||||||
resultSets,
|
resultSets,
|
||||||
database: resultsInfo.database,
|
database: resultsInfo.database,
|
||||||
|
|||||||
@@ -71,6 +71,23 @@ describe('interface-utils', () => {
|
|||||||
|
|
||||||
describe('resolveWholeFileLocation', () => {
|
describe('resolveWholeFileLocation', () => {
|
||||||
it('should resolve a whole file location', () => {
|
it('should resolve a whole file location', () => {
|
||||||
|
const mockDatabaseItem: DatabaseItem = ({
|
||||||
|
resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')),
|
||||||
|
} as unknown) as DatabaseItem;
|
||||||
|
expect(
|
||||||
|
tryResolveLocation(
|
||||||
|
'file://hucairz:0:0:0:0',
|
||||||
|
mockDatabaseItem
|
||||||
|
)
|
||||||
|
).to.deep.equal(
|
||||||
|
new vscode.Location(
|
||||||
|
vscode.Uri.file('abc'),
|
||||||
|
new vscode.Range(0, 0, 0, 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should resolve a five-part location edge case', () => {
|
||||||
const mockDatabaseItem: DatabaseItem = ({
|
const mockDatabaseItem: DatabaseItem = ({
|
||||||
resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')),
|
resolveSourceFile: sinon.stub().returns(vscode.Uri.file('abc')),
|
||||||
} as unknown) as DatabaseItem;
|
} as unknown) as DatabaseItem;
|
||||||
|
|||||||
Reference in New Issue
Block a user