From 73c9d78f602c6fec1f116fc4cd028eb3383a3d46 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Sun, 15 Mar 2020 10:57:25 -0700 Subject: [PATCH] feat: Monaco graphql Completion (#1428) --- .../src/getAutocompleteSuggestions.ts | 69 ++- .../src/getDiagnostics.ts | 14 +- .../src/index.ts | 10 +- packages/monaco-graphql/package.json | 5 +- packages/monaco-graphql/src/graphqlMode.ts | 39 +- packages/monaco-graphql/src/graphqlWorker.ts | 59 +-- .../monaco-graphql/src/languageFeatures.ts | 454 ++---------------- .../monaco-graphql/src/monaco.contribution.ts | 6 +- packages/monaco-graphql/src/monaco.d.ts | 21 +- packages/monaco-graphql/src/typings/refs.d.ts | 1 + packages/monaco-graphql/src/workerManager.ts | 98 ++++ 11 files changed, 230 insertions(+), 546 deletions(-) create mode 100644 packages/monaco-graphql/src/typings/refs.d.ts create mode 100644 packages/monaco-graphql/src/workerManager.ts diff --git a/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts b/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts index 80e28d7f09b..2b469ff41cf 100644 --- a/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts +++ b/packages/graphql-language-service-interface/src/getAutocompleteSuggestions.ts @@ -17,7 +17,6 @@ import { GraphQLCompositeType, GraphQLEnumValue, Kind, - KindEnum, } from 'graphql'; import { @@ -57,6 +56,34 @@ import { objectValues, } from './autocompleteUtils'; +// TODO@acao,rebornix +// Convert AST token kind to Monaco CompletionItemKind +// Should we take `step` into account similar to how we resolve completion items? +function toCompletionItemKind( + kind: string, +): monaco.languages.CompletionItemKind { + const mItemKind = monaco.languages.CompletionItemKind; + + switch (kind) { + case 'Document': + case 'SelectionSet': + case 'Field': + case 'AliasedField': + case 'Arguments': + case 'ObjectValue': + case 'ObjectField': + case 'EnumValue': + case 'ListValue': + case 'ListType': + case 'TypeCondition': + case 'NamedType': + case 'FragmentSpread': + case 'VariableDefinition': + case 'Directive': + return mItemKind.Method; + } +} + /** * Given GraphQLSchema, queryText, and context of the current position within * the source text, provide a list of typeahead entries. @@ -83,20 +110,16 @@ export function getAutocompleteSuggestions( // Definition kinds if (kind === 'Document') { return hintList(token, [ - { label: 'query', kind: CompletionItemKind.Function }, - { label: 'mutation', kind: CompletionItemKind.Function }, - { label: 'subscription', kind: CompletionItemKind.Function }, - { label: 'fragment', kind: CompletionItemKind.Function }, - { label: '{', kind: CompletionItemKind.Constructor }, + { label: 'query', kind: toCompletionItemKind(kind) }, + { label: 'mutation', kind: toCompletionItemKind(kind) }, + { label: 'subscription', kind: toCompletionItemKind(kind) }, + { label: 'fragment', kind: toCompletionItemKind(kind) }, + { label: '{', kind: toCompletionItemKind(kind) }, ]); } // Field names - if ( - kind === RuleKinds.SELECTION_SET || - kind === RuleKinds.FIELD || - kind === RuleKinds.ALIASED_FIELD - ) { + if (kind === 'SelectionSet' || kind === 'Field' || kind === 'AliasedField') { return getSuggestionsForFieldNames(token, typeInfo, schema, kind); } @@ -113,7 +136,7 @@ export function getAutocompleteSuggestions( label: argDef.name, detail: String(argDef.type), documentation: argDef.description, - kind: CompletionItemKind.Variable, + kind: toCompletionItemKind(kind), })), ); } @@ -133,7 +156,7 @@ export function getAutocompleteSuggestions( label: field.name, detail: String(field.type), documentation: field.description, - kind: completionKind, + kind: toCompletionItemKind(kind), })), ); } @@ -200,7 +223,7 @@ function getSuggestionsForFieldNames( token: ContextToken, typeInfo: AllTypeInfo, schema: GraphQLSchema, - kind: RuleKind.SelectionSet | RuleKind.Field | RuleKind.AliasedField, + kind: string, ): Array { if (typeInfo.parentType) { const parentType = typeInfo.parentType; @@ -223,7 +246,7 @@ function getSuggestionsForFieldNames( deprecated: field.isDeprecated, isDeprecated: field.isDeprecated, deprecationReason: field.deprecationReason, - kind: CompletionItemKind.Field, + kind: toCompletionItemKind(kind), })), ); } @@ -248,7 +271,7 @@ function getSuggestionsForInputValues( deprecated: value.isDeprecated, isDeprecated: value.isDeprecated, deprecationReason: value.deprecationReason, - kind: CompletionItemKind.EnumMember, + kind: toCompletionItemKind(kind), }), ), ); @@ -258,14 +281,14 @@ function getSuggestionsForInputValues( label: 'true', detail: String(GraphQLBoolean), documentation: 'Not false.', - kind: CompletionItemKind.Variable, + kind: toCompletionItemKind(kind), }, { label: 'false', detail: String(GraphQLBoolean), documentation: 'Not true.', - kind: CompletionItemKind.Variable, + kind: toCompletionItemKind(kind), }, ]); } @@ -277,7 +300,7 @@ function getSuggestionsForFragmentTypeConditions( token: ContextToken, typeInfo: AllTypeInfo, schema: GraphQLSchema, - kind: Kind.TypeCondition | Kind.NamedType, + kind: string, ): Array { let possibleTypes: GraphQLType[]; if (typeInfo.parentType) { @@ -309,7 +332,7 @@ function getSuggestionsForFragmentTypeConditions( return { label: String(type), documentation: (namedType && namedType.description) || '', - kind: CompletionItemKind.Field, + kind: toCompletionItemKind(kind), }; }), ); @@ -353,7 +376,7 @@ function getSuggestionsForFragmentSpread( label: frag.name.value, detail: String(typeMap[frag.typeCondition.name.value]), documentation: `fragment ${frag.name.value} on ${frag.typeCondition.name.value}`, - kind: CompletionItemKind.Field, + kind: toCompletionItemKind(kind), })), ); } @@ -403,7 +426,7 @@ function getSuggestionsForVariableDefinition( inputTypes.map((type: any) => ({ label: type.name, documentation: type.description, - kind: CompletionItemKind.Variable, + kind: toCompletionItemKind(kind), })), ); } @@ -423,7 +446,7 @@ function getSuggestionsForDirective( directives.map(directive => ({ label: directive.name, documentation: directive.description || '', - kind: CompletionItemKind.Function, + kind: toCompletionItemKind(kind), })), ); } diff --git a/packages/graphql-language-service-interface/src/getDiagnostics.ts b/packages/graphql-language-service-interface/src/getDiagnostics.ts index 7c4c042879b..860d1a1b78b 100644 --- a/packages/graphql-language-service-interface/src/getDiagnostics.ts +++ b/packages/graphql-language-service-interface/src/getDiagnostics.ts @@ -31,10 +31,10 @@ import { import { DiagnosticSeverity, Diagnostic } from 'vscode-languageserver-types'; export const DIAGNOSTIC_SEVERITY = { - ERROR: 1 as DiagnosticSeverity, - WARNING: 2 as DiagnosticSeverity, - INFORMATION: 3 as DiagnosticSeverity, - HINT: 4 as DiagnosticSeverity, + Error: 1 as DiagnosticSeverity, + Warning: 2 as DiagnosticSeverity, + Information: 3 as DiagnosticSeverity, + Hint: 4 as DiagnosticSeverity, }; export function getDiagnostics( @@ -50,7 +50,7 @@ export function getDiagnostics( const range = getRange(error.locations[0], query); return [ { - severity: DIAGNOSTIC_SEVERITY.ERROR as DiagnosticSeverity, + severity: DIAGNOSTIC_SEVERITY.Error as DiagnosticSeverity, message: error.message, source: 'GraphQL: Syntax', range, @@ -74,7 +74,7 @@ export function validateQuery( const validationErrorAnnotations = mapCat( validateWithCustomRules(schema, ast, customRules, isRelayCompatMode), - error => annotations(error, DIAGNOSTIC_SEVERITY.ERROR, 'Validation'), + error => annotations(error, DIAGNOSTIC_SEVERITY.Error, 'Validation'), ); // Note: findDeprecatedUsages was added in graphql@0.9.0, but we want to @@ -82,7 +82,7 @@ export function validateQuery( const deprecationWarningAnnotations = !findDeprecatedUsages ? [] : mapCat(findDeprecatedUsages(schema, ast), error => - annotations(error, DIAGNOSTIC_SEVERITY.WARNING, 'Deprecation'), + annotations(error, DIAGNOSTIC_SEVERITY.Warning, 'Deprecation'), ); return validationErrorAnnotations.concat(deprecationWarningAnnotations); diff --git a/packages/graphql-language-service-interface/src/index.ts b/packages/graphql-language-service-interface/src/index.ts index e0ca018e4ba..e43bb90084e 100644 --- a/packages/graphql-language-service-interface/src/index.ts +++ b/packages/graphql-language-service-interface/src/index.ts @@ -13,9 +13,13 @@ export * from './getAutocompleteSuggestions'; export * from './getDefinition'; -export * from './getDiagnostics'; -export * from './getOutline'; -export * from './getHoverInformation'; +export { + getDiagnostics, + validateQuery, + DIAGNOSTIC_SEVERITY as DiagnosticSeverity, +} from './getDiagnostics'; +export { getOutline } from './getOutline'; +export { getHoverInformation } from './getHoverInformation'; export * from './GraphQLLanguageService'; export * from './GraphQLCache'; diff --git a/packages/monaco-graphql/package.json b/packages/monaco-graphql/package.json index f793f9ab7ab..985ac125d08 100644 --- a/packages/monaco-graphql/package.json +++ b/packages/monaco-graphql/package.json @@ -2,8 +2,9 @@ "name": "@graphql/monaco-graphql", "version": "0.0.1", "dependencies": { - "monaco-editor": "0.20.0", - "monaco-editor-core": "0.20.0", "graphql-language-service-interface": "2.4.0-alpha.1" + }, + "devDependencies": { + "monaco-editor-core": "0.20.0" } } diff --git a/packages/monaco-graphql/src/graphqlMode.ts b/packages/monaco-graphql/src/graphqlMode.ts index d70d1ec676e..6d22feaadeb 100644 --- a/packages/monaco-graphql/src/graphqlMode.ts +++ b/packages/monaco-graphql/src/graphqlMode.ts @@ -1,4 +1,4 @@ -import * as monaco from 'monaco-editor'; +import * as monaco from 'monaco-editor-core'; import IRichLanguageConfiguration = monaco.languages.LanguageConfiguration; @@ -28,22 +28,6 @@ export function setupMode(defaults: LanguageServiceDefaultsImpl): IDisposable { disposeAll(providers); - if (modeConfiguration.documentFormattingEdits) { - providers.push( - monaco.languages.registerDocumentFormattingEditProvider( - languageId, - new languageFeatures.DocumentFormattingEditProvider(worker), - ), - ); - } - if (modeConfiguration.documentRangeFormattingEdits) { - providers.push( - monaco.languages.registerDocumentRangeFormattingEditProvider( - languageId, - new languageFeatures.DocumentRangeFormattingEditProvider(worker), - ), - ); - } if (modeConfiguration.completionItems) { providers.push( monaco.languages.registerCompletionItemProvider( @@ -52,27 +36,6 @@ export function setupMode(defaults: LanguageServiceDefaultsImpl): IDisposable { ), ); } - if (modeConfiguration.hovers) { - providers.push( - monaco.languages.registerHoverProvider( - languageId, - new languageFeatures.HoverAdapter(worker), - ), - ); - } - if (modeConfiguration.documentSymbols) { - providers.push( - monaco.languages.registerDocumentSymbolProvider( - languageId, - new languageFeatures.DocumentSymbolAdapter(worker), - ), - ); - } - if (modeConfiguration.diagnostics) { - providers.push( - new languageFeatures.DiagnosticsAdapter(languageId, worker, defaults), - ); - } } registerProviders(); diff --git a/packages/monaco-graphql/src/graphqlWorker.ts b/packages/monaco-graphql/src/graphqlWorker.ts index 1e4708bce4b..2a99f244956 100644 --- a/packages/monaco-graphql/src/graphqlWorker.ts +++ b/packages/monaco-graphql/src/graphqlWorker.ts @@ -1,5 +1,3 @@ -import * as monaco from 'monaco-editor'; - import IWorkerContext = monaco.worker.IWorkerContext; import * as graphqlService from 'graphql-languageservice'; @@ -26,62 +24,36 @@ export class GraphQLWorker { } async doValidation(uri: string): Promise { - const document = this._getTextDocument(uri); - if (document) { - return this._languageService.getDiagnostics(document, uri); + const query = this._getQueryText(uri); + if (query) { + return this._languageService.getDiagnostics(query, uri); } return Promise.resolve([]); } async doComplete(uri: string, position: Position): Promise { - const document = this._getTextDocument(uri); + const query = this._getQueryText(uri); return this._languageService.getAutocompleteSuggestions( - document, + query, position, uri, ); } - async doHover( - uri: string, - position: graphqlService.Position, - ): Promise { - const document = this._getTextDocument(uri); - return this._languageService.getHoverInformation(document, position, uri); - } - async format( - uri: string, - range: graphqlService.Range, - options: graphqlService.FormattingOptions, - ): Promise { - const document = this._getTextDocument(uri); - const textEdits = this._languageService.format(document, range, options); - return Promise.resolve(textEdits); - } async reloadSchema(uri: string): Promise { - return this._languageService.getConfigForURI(uri).getSchema(); - } - async findDocumentSymbols( - uri: string, - ): Promise { - const document = this._getTextDocument(uri); - const symbols = this._languageService.getDocumentSymbols(document, uri); - return Promise.resolve(symbols); + // TODO@acao, rebornix + // return this._languageService.getConfigForURI(uri).getSchema(); + return false; } - // async getSelectionRanges(uri: string, positions: graphqlService.Position[]): Promise { - // const document = this._getTextDocument(uri); - // const ranges = this._languageService.getSelectionRanges(document, positions, uri); - // return Promise.resolve(ranges); - // } - private _getTextDocument(uri: string): graphqlService.TextDocument { + + resetSchema(uri: string) {} + + private _getQueryText(uri: string): string { + // TODO@acao, rebornix + const models = this._ctx.getMirrorModels(); for (const model of models) { if (model.uri.toString() === uri) { - return graphqlService.TextDocument.create( - uri, - this._languageId, - model.version, - model.getValue(), - ); + return model.getValue(); } } return null; @@ -90,7 +62,6 @@ export class GraphQLWorker { export interface ICreateData { languageId: string; - languageSettings: graphqlService.LanguageSettings; enableSchemaRequest: boolean; } diff --git a/packages/monaco-graphql/src/languageFeatures.ts b/packages/monaco-graphql/src/languageFeatures.ts index 0ef8380747b..a7273f1d20e 100644 --- a/packages/monaco-graphql/src/languageFeatures.ts +++ b/packages/monaco-graphql/src/languageFeatures.ts @@ -1,8 +1,3 @@ -import * as monaco from 'monaco-editor'; - -import { Kind } from 'graphql'; - -import { LanguageServiceDefaultsImpl } from './monaco.contribution'; import { GraphQLWorker } from './graphqlWorker'; import * as graphqlService from 'graphql-languageservice'; @@ -19,142 +14,6 @@ export interface WorkerAccessor { (...more: Uri[]): Thenable; } -// --- diagnostics --- --- - -export class DiagnosticsAdapter { - private _disposables: IDisposable[] = []; - private _listener: { [uri: string]: IDisposable } = Object.create(null); - - constructor( - private _languageId: string, - private _worker: WorkerAccessor, - defaults: LanguageServiceDefaultsImpl, - ) { - const onModelAdd = (model: monaco.editor.IModel): void => { - const modeId = model.getModeId(); - if (modeId !== this._languageId) { - return; - } - - let handle: number; - this._listener[model.uri.toString()] = model.onDidChangeContent(() => { - clearTimeout(handle); - handle = setTimeout(() => this._doValidate(model.uri, modeId), 500); - }); - - this._doValidate(model.uri, modeId); - }; - - const onModelRemoved = (model: monaco.editor.IModel): void => { - monaco.editor.setModelMarkers(model, this._languageId, []); - const uriStr = model.uri.toString(); - const listener = this._listener[uriStr]; - if (listener) { - listener.dispose(); - delete this._listener[uriStr]; - } - }; - - this._disposables.push(monaco.editor.onDidCreateModel(onModelAdd)); - this._disposables.push( - monaco.editor.onWillDisposeModel(model => { - onModelRemoved(model); - this._resetSchema(model.uri); - }), - ); - this._disposables.push( - monaco.editor.onDidChangeModelLanguage(event => { - onModelRemoved(event.model); - onModelAdd(event.model); - this._resetSchema(event.model.uri); - }), - ); - - this._disposables.push( - defaults.onDidChange(_ => { - monaco.editor.getModels().forEach(model => { - if (model.getModeId() === this._languageId) { - onModelRemoved(model); - onModelAdd(model); - } - }); - }), - ); - - this._disposables.push({ - dispose: () => { - monaco.editor.getModels().forEach(onModelRemoved); - for (const key in this._listener) { - this._listener[key].dispose(); - } - }, - }); - - monaco.editor.getModels().forEach(onModelAdd); - } - - public dispose(): void { - this._disposables.forEach(d => d && d.dispose()); - this._disposables = []; - } - - private _resetSchema(resource: Uri): void { - this._worker().then(worker => { - worker.resetSchema(resource.toString()); - }); - } - - private _doValidate(resource: Uri, languageId: string): void { - this._worker(resource) - .then(worker => { - return worker.doValidation(resource.toString()).then(diagnostics => { - const markers = diagnostics.map(d => toDiagnostics(resource, d)); - const model = monaco.editor.getModel(resource); - if (model && model.getModeId() === languageId) { - monaco.editor.setModelMarkers(model, languageId, markers); - } - }); - }) - .then(undefined, err => { - console.error(err); - }); - } -} - -function toSeverity(lsSeverity: number): monaco.MarkerSeverity { - switch (lsSeverity) { - case graphqlService.DIAGNOSTIC_SEVERITY.Error: - return monaco.MarkerSeverity.Error; - case graphqlService.DiagnosticSeverity.Warning: - return monaco.MarkerSeverity.Warning; - case graphqlService.DiagnosticSeverity.Information: - return monaco.MarkerSeverity.Info; - case graphqlService.DiagnosticSeverity.Hint: - return monaco.MarkerSeverity.Hint; - default: - return monaco.MarkerSeverity.Info; - } -} - -function toDiagnostics( - resource: Uri, - diag: graphqlService.Diagnostic, -): monaco.editor.IMarkerData { - const code = - typeof diag.code === 'number' ? String(diag.code) : diag.code; - - return { - severity: toSeverity(diag.severity), - startLineNumber: diag.range.start.line + 1, - startColumn: diag.range.start.character + 1, - endLineNumber: diag.range.end.line + 1, - endColumn: diag.range.end.character + 1, - message: diag.message, - code, - source: diag.source, - }; -} - // --- completion ------ function fromPosition(position: Position): graphqlService.Position { @@ -174,8 +33,15 @@ function fromRange(range: IRange): graphqlService.Range { character: range.startColumn - 1, }, end: { line: range.endLineNumber - 1, character: range.endColumn - 1 }, + containsPosition: pos => { + return monaco.Range.containsPosition(range, { + lineNumber: pos.line, + column: pos.character, + }); + }, }; } + function toRange(range: graphqlService.Range): Range { if (!range) { return void 0; @@ -188,106 +54,6 @@ function toRange(range: graphqlService.Range): Range { ); } -function toCompletionItemKind( - kind: string, -): monaco.languages.CompletionItemKind { - const mItemKind = monaco.languages.CompletionItemKind; - - switch (kind) { - case Kind.FIELD: - return mItemKind.Property; - case graphqlService.CompletionItemKind.Method: - return mItemKind.Method; - case graphqlService.CompletionItemKind.Function: - return mItemKind.Function; - case graphqlService.CompletionItemKind.Constructor: - return mItemKind.Constructor; - case graphqlService.CompletionItemKind.Field: - return mItemKind.Field; - case graphqlService.CompletionItemKind.Variable: - return mItemKind.Variable; - case graphqlService.CompletionItemKind.Class: - return mItemKind.Class; - case Kind.INTE: - return mItemKind.Interface; - case Kind.Unit: - return mItemKind.Unit; - case Kind.Value: - return mItemKind.Value; - case Kind.Enum: - return mItemKind.Enum; - case Kind.Keyword: - return mItemKind.Keyword; - case Kind.Snippet: - return mItemKind.Snippet; - case Kind.Color: - return mItemKind.Color; - case Kind.File: - return mItemKind.File; - case Kind.Reference: - return mItemKind.Reference; - } - return mItemKind.Property; -} - -function fromCompletionItemKind( - kind: monaco.languages.CompletionItemKind, -): typeof Kind { - const mItemKind = monaco.languages.CompletionItemKind; - - switch (kind) { - case mItemKind.Text: - return graphqlService.CompletionItemKind.Text; - case mItemKind.Method: - return graphqlService.CompletionItemKind.Method; - case mItemKind.Function: - return graphqlService.CompletionItemKind.Function; - case mItemKind.Constructor: - return graphqlService.CompletionItemKind.Constructor; - case mItemKind.Field: - return graphqlService.CompletionItemKind.Field; - case mItemKind.Variable: - return graphqlService.CompletionItemKind.Variable; - case mItemKind.Class: - return graphqlService.CompletionItemKind.Class; - case mItemKind.Interface: - return graphqlService.CompletionItemKind.Interface; - case mItemKind.Module: - return graphqlService.CompletionItemKind.Module; - case mItemKind.Property: - return graphqlService.CompletionItemKind.Property; - case mItemKind.Unit: - return graphqlService.CompletionItemKind.Unit; - case mItemKind.Value: - return graphqlService.CompletionItemKind.Value; - case mItemKind.Enum: - return graphqlService.CompletionItemKind.Enum; - case mItemKind.Keyword: - return graphqlService.CompletionItemKind.Keyword; - case mItemKind.Snippet: - return graphqlService.CompletionItemKind.Snippet; - case mItemKind.Color: - return graphqlService.CompletionItemKind.Color; - case mItemKind.File: - return graphqlService.CompletionItemKind.File; - case mItemKind.Reference: - return graphqlService.CompletionItemKind.Reference; - } - return graphqlService.CompletionItemKind.Property; -} - -function toTextEdit( - textEdit: graphqlService.TextEdit, -): monaco.editor.ISingleEditOperation { - if (!textEdit) { - return void 0; - } - return { - range: toRange(textEdit.range), - text: textEdit.newText, - }; -} - export class CompletionAdapter implements monaco.languages.CompletionItemProvider { constructor(private _worker: WorkerAccessor) {} @@ -301,15 +67,14 @@ export class CompletionAdapter position: Position, _context: monaco.languages.CompletionContext, _token: CancellationToken, - ): Promise { + ): Promise { const resource = model.uri; const worker = await this._worker(resource); - const info = worker.doComplete(resource.toString(), fromPosition(position)); - - if (!info) { - return; - } + const completionItems = await worker.doComplete( + resource.toString(), + fromPosition(position), + ); const wordInfo = model.getWordUntilPosition(position); const wordRange = new Range( position.lineNumber, @@ -317,187 +82,26 @@ export class CompletionAdapter position.lineNumber, wordInfo.endColumn, ); - - const items: monaco.languages.CompletionItem[] = info.items.map(entry => { - const item: monaco.languages.CompletionItem = { - label: entry.label, - insertText: entry.insertText || entry.label, - sortText: entry.sortText, - filterText: entry.filterText, - documentation: entry.documentation, - detail: entry.detail, - range: wordRange, - kind: toCompletionItemKind(entry.kind), - }; - if (entry.textEdit) { - item.range = toRange(entry.textEdit.range); - item.insertText = entry.textEdit.newText; - } - if (entry.additionalTextEdits) { - item.additionalTextEdits = entry.additionalTextEdits.map(toTextEdit); - } - if (entry.insertTextFormat === graphqlService.InsertTextFormat.Snippet) { - item.insertTextRules = - monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet; - } - return item; - }); + const items: monaco.languages.CompletionItem[] = completionItems.map( + entry => { + const item: monaco.languages.CompletionItem = { + label: entry.label, + insertText: entry.insertText || entry.label, + sortText: entry.sortText, + filterText: entry.filterText, + documentation: entry.documentation, + detail: entry.detail, + range: wordRange, + kind: entry.kind, + }; + + return item; + }, + ); return { - incomplete: info.isIncomplete, + incomplete: true, suggestions: items, }; } } - -function isMarkupContent(thing: any): thing is graphqlService.MarkupContent { - return ( - thing && - typeof thing === 'object' && - typeof (thing).kind === 'string' - ); -} - -function toMarkdownString( - entry: graphqlService.MarkupContent | graphqlService.MarkedString, -): monaco.IMarkdownString { - if (typeof entry === 'string') { - return { - value: entry, - }; - } - if (isMarkupContent(entry)) { - if (entry.kind === 'plaintext') { - return { - value: entry.value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'), - }; - } - return { - value: entry.value, - }; - } - - return { value: '```' + entry.language + '\n' + entry.value + '\n```\n' }; -} - -function toMarkedStringArray( - contents: - | graphqlService.MarkupContent - | graphqlService.MarkedString - | graphqlService.MarkedString[], -): monaco.IMarkdownString[] { - if (!contents) { - return void 0; - } - if (Array.isArray(contents)) { - return contents.map(toMarkdownString); - } - return [toMarkdownString(contents)]; -} - -export class HoverAdapter implements monaco.languages.HoverProvider { - constructor(private _worker: WorkerAccessor) {} - - async provideHover( - model: monaco.editor.IReadOnlyModel, - position: Position, - _token: CancellationToken, - ): Promise { - const resource = model.uri; - const worker = await this._worker(resource); - const info = await worker.doHover(resource.toString(), position); - - if (!info) { - return; - } - return { - range: toRange(info.range), - contents: toMarkedStringArray(info.contents), - }; - } -} - -// --- definition ------ - -function toLocation( - location: graphqlService.Location, -): monaco.languages.Location { - return { - uri: Uri.parse(location.uri), - range: toRange(location.range), - }; -} - -// --- document symbols ------ - -function toSymbolKind( - kind: graphqlService.SymbolKind, -): monaco.languages.SymbolKind { - const mKind = monaco.languages.SymbolKind; - - switch (kind) { - case graphqlService.SymbolKind.File: - return mKind.Array; - case graphqlService.SymbolKind.Module: - return mKind.Module; - case graphqlService.SymbolKind.Namespace: - return mKind.Namespace; - case graphqlService.SymbolKind.Package: - return mKind.Package; - case graphqlService.SymbolKind.Class: - return mKind.Class; - case graphqlService.SymbolKind.Method: - return mKind.Method; - case graphqlService.SymbolKind.Property: - return mKind.Property; - case graphqlService.SymbolKind.Field: - return mKind.Field; - case graphqlService.SymbolKind.Constructor: - return mKind.Constructor; - case graphqlService.SymbolKind.Enum: - return mKind.Enum; - case graphqlService.SymbolKind.Interface: - return mKind.Interface; - case graphqlService.SymbolKind.Function: - return mKind.Function; - case graphqlService.SymbolKind.Variable: - return mKind.Variable; - case graphqlService.SymbolKind.Constant: - return mKind.Constant; - case graphqlService.SymbolKind.String: - return mKind.String; - case graphqlService.SymbolKind.Number: - return mKind.Number; - case graphqlService.SymbolKind.Boolean: - return mKind.Boolean; - case graphqlService.SymbolKind.Array: - return mKind.Array; - } - return mKind.Function; -} - -export class DocumentSymbolAdapter - implements monaco.languages.DocumentSymbolProvider { - constructor(private _worker: WorkerAccessor) {} - - public async provideDocumentSymbols( - model: monaco.editor.IReadOnlyModel, - _token: CancellationToken, - ): Promise { - const resource = model.uri; - const worker = await this._worker(resource); - const items = await worker.findDocumentSymbols(resource.toString()); - if (!items) { - return []; - } - return items.map(item => ({ - name: item.name, - detail: '', - containerName: item.containerName, - kind: toSymbolKind(item.kind), - range: toRange(item.location.range), - selectionRange: toRange(item.location.range), - tags: [], - })); - } -} diff --git a/packages/monaco-graphql/src/monaco.contribution.ts b/packages/monaco-graphql/src/monaco.contribution.ts index bb6f2122669..002fcd27441 100644 --- a/packages/monaco-graphql/src/monaco.contribution.ts +++ b/packages/monaco-graphql/src/monaco.contribution.ts @@ -1,4 +1,3 @@ -import * as monaco from 'monaco-editor'; import * as mode from './graphqlMode'; import Emitter = monaco.Emitter; @@ -70,9 +69,10 @@ const modeConfigurationDefault: Required any, thisArg?: any): IDisposable; } export interface DiagnosticsOptions { + /** + * If set, the validator will be enabled and perform syntax validation as well as schema based validation. + */ + readonly validate?: boolean; + /** + * If set, comments are tolerated. If set to false, syntax errors will be emitted for comments. + */ + readonly allowComments?: boolean; /** * A list of known schemas and/or associations of schemas to file names. */ @@ -21,7 +29,7 @@ declare module monaco.languages.graphql { */ readonly uri: string; /** - * A list of file names that are associated to the schema. The '*' wildcard can be used. For example '*.schema.graphql', 'package.graphql' + * A list of file names that are associated to the schema. The '*' wildcard can be used. For example '*.schema.json', 'package.json' */ readonly fileMatch?: string[]; /** @@ -61,6 +69,16 @@ declare module monaco.languages.graphql { */ readonly documentSymbols?: boolean; + /** + * Defines whether the built-in tokens provider is enabled. + */ + readonly tokens?: boolean; + + /** + * Defines whether the built-in color provider is enabled. + */ + readonly colors?: boolean; + /** * Defines whether the built-in foldingRange provider is enabled. */ @@ -76,6 +94,7 @@ declare module monaco.languages.graphql { */ readonly selectionRanges?: boolean; } + export interface LanguageServiceDefaults { readonly onDidChange: IEvent; readonly diagnosticsOptions: DiagnosticsOptions; diff --git a/packages/monaco-graphql/src/typings/refs.d.ts b/packages/monaco-graphql/src/typings/refs.d.ts new file mode 100644 index 00000000000..7fc451932f6 --- /dev/null +++ b/packages/monaco-graphql/src/typings/refs.d.ts @@ -0,0 +1 @@ +// / diff --git a/packages/monaco-graphql/src/workerManager.ts b/packages/monaco-graphql/src/workerManager.ts new file mode 100644 index 00000000000..5577433bf35 --- /dev/null +++ b/packages/monaco-graphql/src/workerManager.ts @@ -0,0 +1,98 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *-------------------------------------------------------------------------------------------- */ +'use strict'; + +import { LanguageServiceDefaultsImpl } from './monaco.contribution'; +import { GraphQLWorker } from './graphqlWorker'; + +import IDisposable = monaco.IDisposable; +import Uri = monaco.Uri; + +const STOP_WHEN_IDLE_FOR = 2 * 60 * 1000; // 2min + +export class WorkerManager { + private _defaults: LanguageServiceDefaultsImpl; + private _idleCheckInterval: number; + private _lastUsedTime: number; + private _configChangeListener: IDisposable; + + private _worker: monaco.editor.MonacoWebWorker | null; + private _client: Promise | null; + + constructor(defaults: LanguageServiceDefaultsImpl) { + this._defaults = defaults; + this._worker = null; + this._idleCheckInterval = (setInterval( + () => this._checkIfIdle(), + 30 * 1000, + ) as unknown) as number; + this._lastUsedTime = 0; + this._configChangeListener = this._defaults.onDidChange(() => + this._stopWorker(), + ); + this._client = null; + } + + private _stopWorker(): void { + if (this._worker) { + this._worker.dispose(); + this._worker = null; + } + this._client = null; + } + + dispose(): void { + clearInterval(this._idleCheckInterval); + this._configChangeListener.dispose(); + this._stopWorker(); + } + + private _checkIfIdle(): void { + if (!this._worker) { + return; + } + const timePassedSinceLastUsed = Date.now() - this._lastUsedTime; + if (timePassedSinceLastUsed > STOP_WHEN_IDLE_FOR) { + this._stopWorker(); + } + } + + private _getClient(): Promise { + this._lastUsedTime = Date.now(); + + if (!this._client) { + this._worker = monaco.editor.createWebWorker({ + // module that exports the create() method and returns a `JSONWorker` instance + moduleId: 'vs/language/json/jsonWorker', + + label: this._defaults.languageId, + + // passed in to the create() method + createData: { + languageSettings: this._defaults.diagnosticsOptions, + languageId: this._defaults.languageId, + enableSchemaRequest: this._defaults.diagnosticsOptions + .enableSchemaRequest, + }, + }); + + this._client = >(this._worker.getProxy()); + } + + return this._client; + } + + getLanguageServiceWorker(...resources: Uri[]): Promise { + let _client: GraphQLWorker; + return this._getClient() + .then(client => { + _client = client; + }) + .then(_ => { + return this._worker!.withSyncedResources(resources); + }) + .then(_ => _client); + } +}