Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(language-service): improve outline in embedded graphql #3848

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
7 changes: 7 additions & 0 deletions .changeset/soft-eggs-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'graphql-language-service-server': patch
'graphql-language-service': patch
'vscode-graphql': patch
---

Fix positions for outlines of embedded graphql documents
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*
*/

import * as path from 'node:path';
import {
DocumentNode,
FragmentSpreadNode,
Expand All @@ -22,6 +23,7 @@ import {
isTypeDefinitionNode,
ArgumentNode,
typeFromAST,
Source as GraphQLSource,
} from 'graphql';

import {
Expand All @@ -47,6 +49,7 @@ import {
getTypeInfo,
DefinitionQueryResponse,
getDefinitionQueryResultForArgument,
IRange,
} from 'graphql-language-service';

import type { GraphQLCache } from './GraphQLCache';
Expand Down Expand Up @@ -359,8 +362,13 @@ export class GraphQLLanguageService {
public async getDocumentSymbols(
document: string,
filePath: Uri,
fileDocumentRange?: IRange | null,
): Promise<SymbolInformation[]> {
const outline = await this.getOutline(document);
const outline = await this.getOutline(
document,
path.basename(filePath),
fileDocumentRange?.start,
);
if (!outline) {
return [];
}
Expand All @@ -379,14 +387,12 @@ export class GraphQLLanguageService {
}

output.push({
// @ts-ignore
name: tree.representativeName ?? 'Anonymous',
kind: getKind(tree),
location: {
uri: filePath,
range: {
start: tree.startPosition,
// @ts-ignore
end: tree.endPosition,
},
},
Expand Down Expand Up @@ -539,7 +545,20 @@ export class GraphQLLanguageService {
);
}

async getOutline(documentText: string): Promise<Outline | null> {
return getOutline(documentText);
async getOutline(
documentText: string,
documentName: string,
documentOffset?: IPosition,
): Promise<Outline | null> {
return getOutline(
new GraphQLSource(
documentText,
documentName,
documentOffset && {
column: documentOffset.character + 1,
line: documentOffset.line + 1,
},
),
);
}
}
97 changes: 61 additions & 36 deletions packages/graphql-language-service-server/src/MessageProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@
LoaderNoResultError,
ProjectNotFoundError,
} from 'graphql-config';
import type { LoadConfigOptions, LocateCommand } from './types';
import type {
LoadConfigOptions,
LocateCommand,
VSCodeGraphQLConfigLoadSettings,
VSCodeGraphQLSettings,
} from './types';
import {
DEFAULT_SUPPORTED_EXTENSIONS,
SupportedExtensionsEnum,
Expand All @@ -83,12 +88,20 @@
type CachedDocumentType = {
version: number;
contents: CachedContent[];
size: number;
};

function toPosition(position: VscodePosition): IPosition {
return new Position(position.line, position.character);
}

interface MessageProcessorSettings extends VSCodeGraphQLSettings {
load: VSCodeGraphQLConfigLoadSettings & {
fileName?: string;
[key: string]: unknown;
};
}

export class MessageProcessor {
private _connection: Connection;
private _graphQLCache!: GraphQLCache;
Expand All @@ -103,7 +116,7 @@
private _tmpDirBase: string;
private _loadConfigOptions: LoadConfigOptions;
private _rootPath: string = process.cwd();
private _settings: any;
private _settings: MessageProcessorSettings = { load: {} };
private _providedConfig?: GraphQLConfig;

constructor({
Expand Down Expand Up @@ -210,7 +223,7 @@
// TODO: eventually we will instantiate an instance of this per workspace,
// so rootDir should become that workspace's rootDir
this._settings = { ...settings, ...vscodeSettings };
const rootDir = this._settings?.load?.rootDir.length
const rootDir = this._settings?.load?.rootDir?.length
? this._settings?.load?.rootDir
: this._rootPath;
if (settings?.dotEnvPath) {
Expand Down Expand Up @@ -486,17 +499,11 @@

// As `contentChanges` is an array, and we just want the
// latest update to the text, grab the last entry from the array.

// If it's a .js file, try parsing the contents to see if GraphQL queries
// exist. If not found, delete from the cache.
const { contents } = await this._parseAndCacheFile(
uri,
project,
contentChanges.at(-1)!.text,
);
// // If it's a .graphql file, proceed normally and invalidate the cache.
// await this._invalidateCache(textDocument, uri, contents);

const diagnostics: Diagnostic[] = [];

if (project?.extensions?.languageService?.enableValidation !== false) {
Expand Down Expand Up @@ -706,7 +713,10 @@
const contents = await this._parser(fileText, uri);
const cachedDocument = this._textDocumentCache.get(uri);
const version = cachedDocument ? cachedDocument.version++ : 0;
await this._invalidateCache({ uri, version }, uri, contents);
await this._invalidateCache(
{ uri, version },
{ contents, size: fileText.length },
);
await this._updateFragmentDefinition(uri, contents);
await this._updateObjectTypeDefinition(uri, contents, project);
await this._updateSchemaIfChanged(project, uri);
Expand Down Expand Up @@ -942,14 +952,13 @@

const { textDocument } = params;
const cachedDocument = this._getCachedDocument(textDocument.uri);
if (!cachedDocument?.contents[0]) {
if (!cachedDocument?.contents?.length) {
return [];
}

if (
this._settings.largeFileThreshold !== undefined &&
this._settings.largeFileThreshold <
cachedDocument.contents[0].query.length
this._settings.largeFileThreshold < cachedDocument.size

Check warning on line 961 in packages/graphql-language-service-server/src/MessageProcessor.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphql-language-service-server/src/MessageProcessor.ts#L961

Added line #L961 was not covered by tests
) {
return [];
}
Expand All @@ -962,10 +971,16 @@
}),
);

return this._languageService.getDocumentSymbols(
cachedDocument.contents[0].query,
textDocument.uri,
const results = await Promise.all(
cachedDocument.contents.map(content =>
this._languageService.getDocumentSymbols(
content.query,
textDocument.uri,
content.range,
),
),
);
return results.flat();
}

// async handleReferencesRequest(params: ReferenceParams): Promise<Location[]> {
Expand Down Expand Up @@ -1003,14 +1018,25 @@
documents.map(async ([uri]) => {
const cachedDocument = this._getCachedDocument(uri);

if (!cachedDocument) {
if (!cachedDocument?.contents?.length) {
return [];
}
const docSymbols = await this._languageService.getDocumentSymbols(
cachedDocument.contents[0].query,
uri,
if (
this._settings.largeFileThreshold !== undefined &&
this._settings.largeFileThreshold < cachedDocument.size

Check warning on line 1026 in packages/graphql-language-service-server/src/MessageProcessor.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphql-language-service-server/src/MessageProcessor.ts#L1026

Added line #L1026 was not covered by tests
) {
return [];

Check warning on line 1028 in packages/graphql-language-service-server/src/MessageProcessor.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphql-language-service-server/src/MessageProcessor.ts#L1028

Added line #L1028 was not covered by tests
}
const docSymbols = await Promise.all(
cachedDocument.contents.map(content =>
this._languageService.getDocumentSymbols(
content.query,
uri,
content.range,
),
),
);
symbols.push(...docSymbols);
symbols.push(...docSymbols.flat());
}),
);
return symbols.filter(symbol => symbol?.name?.includes(params.query));
Expand All @@ -1032,7 +1058,10 @@
try {
const contents = await this._parser(text, uri);
if (contents.length > 0) {
await this._invalidateCache({ version, uri }, uri, contents);
await this._invalidateCache(
{ version, uri },
{ contents, size: text.length },
);
await this._updateObjectTypeDefinition(uri, contents, project);
}
} catch (err) {
Expand Down Expand Up @@ -1244,7 +1273,10 @@
}
await this._updateObjectTypeDefinition(uri, contents);
await this._updateFragmentDefinition(uri, contents);
await this._invalidateCache({ version: 1, uri }, uri, contents);
await this._invalidateCache(
{ version: 1, uri },
{ contents, size: document.rawSDL.length },
);
}),
);
} catch (err) {
Expand Down Expand Up @@ -1353,27 +1385,20 @@
}
private async _invalidateCache(
textDocument: VersionedTextDocumentIdentifier,
uri: Uri,
contents: CachedContent[],
meta: Omit<CachedDocumentType, 'version'>,
): Promise<Map<string, CachedDocumentType> | null> {
const { uri, version } = textDocument;
if (this._textDocumentCache.has(uri)) {
const cachedDocument = this._textDocumentCache.get(uri);
if (
cachedDocument &&
textDocument?.version &&
cachedDocument.version < textDocument.version
) {
if (cachedDocument && version && cachedDocument.version < version) {
// Current server capabilities specify the full sync of the contents.
// Therefore always overwrite the entire content.
return this._textDocumentCache.set(uri, {
version: textDocument.version,
contents,
});
return this._textDocumentCache.set(uri, { ...meta, version });

Check warning on line 1396 in packages/graphql-language-service-server/src/MessageProcessor.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphql-language-service-server/src/MessageProcessor.ts#L1396

Added line #L1396 was not covered by tests
}
}
return this._textDocumentCache.set(uri, {
version: textDocument.version ?? 0,
contents,
...meta,
version: version ?? 0,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql-language-service-server/src/parseDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ export async function parseDocument(
return [];
}

// If it's a .js file, parse the contents to see if GraphQL queries exist.
if (fileExtensions.includes(ext)) {
const templates = await findGraphQLTags(text, ext, uri, logger);
return templates.map(({ template, range }) => ({ query: template, range }));
}
// If it's a .graphql file, use the entire file
if (graphQLFileExtensions.includes(ext)) {
const query = text;
const lines = query.split('\n');
Expand Down
42 changes: 42 additions & 0 deletions packages/graphql-language-service-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,45 @@ export interface ServerOptions {
*/
debug?: true;
}

export interface VSCodeGraphQLSettings {
/**
* Enable debug logs and node debugger for client
*/
debug?: boolean | null;
/**
* Use a cached file output of your graphql-config schema result for definition lookups, symbols, outline, etc. Enabled by default when one or more schema entry is not a local file with SDL in it. Disable if you want to use SDL with a generated schema.
*/
cacheSchemaFileForLookup?: boolean;
/**
* Disables outlining and other expensive operations for files larger than this threshold (in bytes). Defaults to 1000000 (one million).
*/
largeFileThreshold?: number;
/**
* Fail the request on invalid certificate
*/
rejectUnauthorized?: boolean;
/**
* Schema cache ttl in milliseconds - the interval before requesting a fresh schema when caching the local schema file is enabled. Defaults to 30000 (30 seconds).
*/
schemaCacheTTL?: number;
}

export interface VSCodeGraphQLConfigLoadSettings {
/**
* Base dir for graphql config loadConfig(), to look for config files or package.json
*/
rootDir?: string;
/**
* exact filePath for a `graphql-config` file `loadConfig()`
*/
filePath?: string;
/**
* optional <configName>.config.{js,ts,toml,yaml,json} & <configName>rc* instead of default `graphql`
*/
configName?: string;
/**
* legacy mode for graphql config v2 config
*/
legacy?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {

import { Definition, FragmentInfo, Uri, ObjectTypeInfo } from '../types';

import { locToRange, offsetToPosition, Range, Position } from '../utils';
import { locToRange, locStartToPosition, Range, Position } from '../utils';
// import { getTypeInfo } from './getAutocompleteSuggestions';

export type DefinitionQueryResult = {
Expand Down Expand Up @@ -56,7 +56,7 @@ function getRange(text: string, node: ASTNode): Range {
function getPosition(text: string, node: ASTNode): Position {
const location = node.loc!;
assert(location, 'Expected ASTNode to have a location.');
return offsetToPosition(text, location.start);
return locStartToPosition(text, location);
}

export async function getDefinitionQueryResultForNamedType(
Expand Down
Loading
Loading