Skip to content
This repository has been archived by the owner on Sep 2, 2020. It is now read-only.

Commit

Permalink
Have graphql-language-service spec-compliant to Language Server Proto…
Browse files Browse the repository at this point in the history
…col (#8)
  • Loading branch information
asiandrummer authored Feb 1, 2017
1 parent ec1ecbd commit 820b03c
Show file tree
Hide file tree
Showing 20 changed files with 793 additions and 537 deletions.
2 changes: 0 additions & 2 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
!.eslintrc.js
**/node_modules/**
**/VendorLib/**
**/flow-typed/**
pkg/nuclide-external-interfaces/1.0/simple-text-buffer.js
135 changes: 40 additions & 95 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

_This is currently in technical preview. We welcome your feedback and suggestions._

GraphQL Language Service provides an interface for building GraphQL language services for IDEs. Currently supported features include:
- Diagnostics (GraphQL syntax linting/validations)
- Autocomplete suggestions
GraphQL Language Service provides an interface for building GraphQL language service for IDEs.

A subset of supported features of GraphQL language service and GraphQL language server implementation are both specification-compliant to [Microsoft's Language Server Protocol](https://github.com/Microsoft/language-server-protocol), and will be developed to fully support the specification in the future.

Currently supported features include:
- Diagnostics (GraphQL syntax linting/validations) (**spec-compliant**)
- Autocomplete suggestions (**spec-compliant**)
- Hyperlink to fragment definitions
- Outline view support for queries

Expand Down Expand Up @@ -61,31 +65,42 @@ The node executable contains several commands: `server` and a command-line langu
Improving this list is a work-in-progress.

```
Usage: graphql <command>
GraphQL Language Service Command-Line Interface.
Usage: bin/graphql.js <command> <file>
[-h | --help]
[-c | --config] {configPath}
[-c | --configDir] {configDir}
[-t | --text] {textBuffer}
[-f | --file] {filePath}
[-s | --schema] {schemaPath}
Options:
-h, --help Show help [boolean]
-c, --config GraphQL Config file path (.graphqlrc).
Will look for the nearest .graphqlrc file if omitted.
-h, --help Show help [boolean]
-t, --text Text buffer to perform GraphQL diagnostics on.
Will defer to --file option if omitted.
This option is always honored over --file option.
[string]
-t, --text Text buffer to perform GraphQL lint on.
Will defer to --file option if omitted.
This option is always honored over --file option.
-f, --file File path to perform GraphQL diagnostics on.
Will be ignored if --text option is supplied.
[string]
-f, --file File path to perform GraphQL lint on.
Will be ignored if --text option is supplied.
--row A row number from the cursor location for GraphQL
autocomplete suggestions.
If omitted, the last row number will be used.
[number]
--column A column number from the cursor location for GraphQL
autocomplete suggestions.
If omitted, the last column number will be used.
[number]
-c, --configDir A directory path where .graphqlrc configuration object is
Walks up the directory tree from the provided config
directory, or the current working directory, until
.graphqlrc is found or the root directory is found.
[string]
-s, --schema a path to schema DSL file
-s, --schemaPath a path to schema DSL file
[string]
At least one command is required.
Commands: "server, lint, autocomplete, outline"
Commands: "server, validate, autocomplete, outline"
```

## Architectural Overview
Expand All @@ -112,84 +127,14 @@ The IDE server should manage the lifecycle of the GraphQL server. Ideally, the I

### Server Interface

The server sends/receives RPC messages to/from the IDE server to perform language service features. The details for the RPC message format are described below:

```
/**
* The JSON message sent from the IDE server should have the following structure:
* {
* protocol: 'graphql-protocol',
* id: number,
* method: string, // one of the function names below, e.g. `getDiagnostics`
* args: {
* query?: string,
* position?: Point,
* filePath?: Uri,
* }
* }
*/
// Diagnostics (lint/validation)
export type GraphQLDiagnosticMessage = {
name: string,
type: string,
text: string,
range: atom$Range,
filePath: string,
};
export function getDiagnostics(
query: string,
filePath: Uri,
) : Promise<Array<GraphQLDiagnosticMessage>> {
throw new Error('RPC stub');
}
// Autocomplete Suggestions (typeahead)
export type GraphQLAutocompleteSuggestionType = {
text: string,
typeName: ?string,
description: ?string,
};
export function getAutocompleteSuggestions(
query: string,
position: atom$Point,
filePath: Uri,
) : Promise<Array<GraphQLAutocompleteSuggestionType>> {
throw new Error('RPC stub');
}
GraphQL Language Server uses [JSON-RPC](http://www.jsonrpc.org/specification) to communicate with the IDE servers to perform language service features. The language server currently supports two communication transports: Stream (stdio) and IPC. For IPC transport, the reference guide to be used for development is [the language server protocol](https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md) documentation.

// Definitions (hyperlink)
export type Definition = {
path: Uri,
position: Point,
range?: Range,
id?: string,
name?: string,
language: string,
projectRoot?: Uri,
};
export type DefinitionQueryResult = {
queryRange: Array<Range>,
definitions: Array<Definition>,
};
export function getDefinition(
query: string,
position: atom$Point,
filePath: Uri,
): Promise<DefinitionQueryResult> {
throw new Error('RPC stub');
}
For each transports, there is a slight difference between both JSON message format, especially in how the methods to be invoked are defined - below are the currently supported methods for each transports (will be updated as progresses are made):

// Outline view
export function getOutline(query: string): Promise<Outline> {
throw new Error('RPC stub');
}
// Disconnect signal - gracefully terminate the connection on IDE exit
export function disconnect(): void {
throw new Error('RPC stub');
}
```
| | Stream | IPC |
| -------------------:|------------------------------|-----------------------------------|
| Diagnostics | `getDiagnostics` | `textDocument/publishDiagnostics` |
| Autocompletion | `getAutocompleteSuggestions` | `textDocument/completion` |
| Outline | `getOutline` | Not supported yet |
| Go-to definition | `getDefinition` | Not supported yet |
| File Events | Not supported yet | `didOpen/didClose/didSave/didChange` events |
12 changes: 10 additions & 2 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const {argv} = yargs
'GraphQL Language Service Command-Line Interface.\n' +
'Usage: $0 <command> <file>\n' +
' [-h | --help]\n' +
' [-c | --config] {configPath}\n' +
' [-c | --configDir] {configDir}\n' +
' [-t | --text] {textBuffer}\n' +
' [-f | --file] {filePath}\n' +
' [-s | --schema] {schemaPath}\n',
Expand Down Expand Up @@ -52,6 +52,14 @@ const {argv} = yargs
'If omitted, the last column number will be used.\n',
type: 'number',
})
.option('c', {
alias: 'configDir',
describe: 'A directory path where .graphqlrc configuration object is\n' +
'Walks up the directory tree from the provided config directory, or ' +
'the current working directory, until .graphqlrc is found or ' +
'the root directory is found.\n',
type: 'string',
})
.option('s', {
alias: 'schemaPath',
describe: 'a path to schema DSL file\n',
Expand All @@ -62,7 +70,7 @@ const command = argv._.pop();

switch (command) {
case 'server':
startServer(argv.config.trim());
startServer(argv.configDir);
break;
default:
client(command, argv);
Expand Down
8 changes: 4 additions & 4 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import fs from 'fs';
import {buildSchema, buildClientSchema} from 'graphql';
import path from 'path';

import {Point} from './utils/Range';
import {Position} from './utils/Range';
import {
getAutocompleteSuggestions,
} from './interfaces/getAutocompleteSuggestions';
Expand Down Expand Up @@ -54,7 +54,7 @@ export default function main(command: string, argv: Object): void {
const lines = text.split('\n');
const row = argv.row || lines.length - 1;
const column = argv.column || lines[lines.length - 1].length;
const point = new Point(row, column);
const point = new Position(row, column);
exitCode = _getAutocompleteSuggestions(text, point, schemaPath);
break;
case 'outline':
Expand All @@ -72,7 +72,7 @@ export default function main(command: string, argv: Object): void {

function _getAutocompleteSuggestions(
queryText: string,
point: Point,
point: Position,
schemaPath: string,
): EXIT_CODE {
invariant(
Expand Down Expand Up @@ -104,7 +104,7 @@ function _getDiagnostics(
// `schema` is not strictly requied as GraphQL diagnostics may still notify
// whether the query text is syntactically valid.
const schema = schemaPath ? generateSchema(schemaPath) : null;
const resultArray = getDiagnostics(filePath, queryText, schema);
const resultArray = getDiagnostics(queryText, schema);
const resultObject = resultArray.reduce((prev, cur, index) => {
prev[index] = cur;
return prev;
Expand Down
20 changes: 20 additions & 0 deletions src/config/GraphQLConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ const CONFIG_LIST_NAME = 'build-configs';
const SCHEMA_PATH = 'schema-file';
const CUSTOM_VALIDATION_RULES_MODULE_PATH = 'custom-validation-rules';

/**
* Finds a .graphqlrc configuration file, and returns null if not found.
* If the file isn't present in the provided directory path, walk up the
* directory tree until the file is found or it reaches the root directory.
*/
export async function findGraphQLConfigDir(dirPath: Uri): Promise<?string> {
let currentPath = path.resolve(dirPath);
let filePath;
while (currentPath.length > 1) {
filePath = path.join(currentPath, '.graphqlrc');
if (fs.existsSync(filePath)) {
break;
}

currentPath = path.dirname(currentPath);
}

return filePath ? currentPath : null;
}

export async function getGraphQLConfig(configDir: Uri): Promise<GraphQLRC> {
const rawGraphQLConfig = await new Promise((resolve, reject) =>
fs.readFile(
Expand Down
24 changes: 12 additions & 12 deletions src/interfaces/GraphQLLanguageService.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import type {ASTNode} from 'graphql/language';
import type {GraphQLCache} from '../server/GraphQLCache';
import type {GraphQLRC, GraphQLConfig} from '../config/GraphQLConfig';
import type {
AutocompleteSuggestionType,
CompletionItem,
DefinitionQueryResult,
DiagnosticType,
Diagnostic,
Uri,
} from '../types/Types';
import type {Point} from '../utils/Range';
import type {Position} from '../utils/Range';

import {
FRAGMENT_SPREAD,
Expand All @@ -32,7 +32,7 @@ import {
getDefinitionQueryResultForFragmentSpread,
getDefinitionQueryResultForDefinitionNode,
} from './getDefinition';
import {getASTNodeAtPoint} from '../utils/getASTNodeAtPoint';
import {getASTNodeAtPosition} from '../utils/getASTNodeAtPosition';

export class GraphQLLanguageService {
_graphQLCache: GraphQLCache;
Expand All @@ -45,10 +45,10 @@ export class GraphQLLanguageService {

async getDiagnostics(
query: string,
filePath: Uri,
): Promise<Array<DiagnosticType>> {
uri: Uri,
): Promise<Array<Diagnostic>> {
let source = query;
const graphQLConfig = this._graphQLRC.getConfigByFilePath(filePath);
const graphQLConfig = this._graphQLRC.getConfigByFilePath(uri);
// If there's a matching config, proceed to prepare to run validation
let schema;
let customRules;
Expand Down Expand Up @@ -80,14 +80,14 @@ export class GraphQLLanguageService {
}
}

return getDiagnosticsImpl(filePath, source, schema, customRules);
return getDiagnosticsImpl(source, schema, customRules);
}

async getAutocompleteSuggestions(
query: string,
position: Point,
position: Position,
filePath: Uri,
): Promise<Array<AutocompleteSuggestionType>> {
): Promise<Array<CompletionItem>> {
const graphQLConfig = this._graphQLRC.getConfigByFilePath(filePath);
let schema;
if (graphQLConfig && graphQLConfig.getSchemaPath()) {
Expand All @@ -102,7 +102,7 @@ export class GraphQLLanguageService {

async getDefinition(
query: string,
position: Point,
position: Position,
filePath: Uri,
): Promise<?DefinitionQueryResult> {
const graphQLConfig = this._graphQLRC.getConfigByFilePath(filePath);
Expand All @@ -117,7 +117,7 @@ export class GraphQLLanguageService {
return null;
}

const node = getASTNodeAtPoint(query, ast, position);
const node = getASTNodeAtPosition(query, ast, position);
switch (node ? node.kind : null) {
case FRAGMENT_SPREAD:
return this._getDefinitionForFragmentSpread(
Expand Down
Loading

0 comments on commit 820b03c

Please sign in to comment.