apollo-language-server
Version:
A language server for Apollo GraphQL projects
237 lines (202 loc) • 6.48 kB
text/typescript
import "apollo-env";
// FIXME: The global fetch dependency comes from `apollo-link-http` and should be removed there.
import "apollo-env/lib/fetch/global";
import {
createConnection,
ProposedFeatures,
FileChangeType,
ServerCapabilities,
TextDocuments,
TextDocumentSyncKind,
} from "vscode-languageserver/node";
import { TextDocument } from "vscode-languageserver-textdocument";
import { QuickPickItem } from "vscode";
import { GraphQLWorkspace } from "./workspace";
import { GraphQLLanguageProvider } from "./languageProvider";
import { LanguageServerLoadingHandler } from "./loadingHandler";
import { debounceHandler, Debug } from "./utilities";
const connection = createConnection(ProposedFeatures.all);
Debug.SetConnection(connection);
let hasWorkspaceFolderCapability = false;
// Awaitable promise for sending messages before the connection is initialized
let initializeConnection: () => void;
const whenConnectionInitialized: Promise<void> = new Promise(
(resolve) => (initializeConnection = resolve)
);
const workspace = new GraphQLWorkspace(
new LanguageServerLoadingHandler(connection),
{
clientIdentity: {
name: process.env["APOLLO_CLIENT_NAME"],
version: process.env["APOLLO_CLIENT_VERSION"],
referenceID: process.env["APOLLO_CLIENT_REFERENCE_ID"],
},
}
);
workspace.onDiagnostics((params) => {
connection.sendDiagnostics(params);
});
workspace.onDecorations((params) => {
connection.sendNotification("apollographql/engineDecorations", params);
});
workspace.onSchemaTags((params) => {
connection.sendNotification(
"apollographql/tagsLoaded",
JSON.stringify(params)
);
});
workspace.onConfigFilesFound(async (params) => {
await whenConnectionInitialized;
connection.sendNotification(
"apollographql/configFilesFound",
params instanceof Error
? // Can't stringify Errors, just results in "{}"
JSON.stringify({ message: params.message, stack: params.stack })
: JSON.stringify(params)
);
});
connection.onInitialize(async ({ capabilities, workspaceFolders }) => {
hasWorkspaceFolderCapability = !!(
capabilities.workspace && capabilities.workspace.workspaceFolders
);
if (workspaceFolders) {
// We wait until all projects are added, because after `initialize` returns we can get additional requests
// like `textDocument/codeLens`, and that way these can await `GraphQLProject#whenReady` to make sure
// we provide them eventually.
await Promise.all(
workspaceFolders.map((folder) => workspace.addProjectsInFolder(folder))
);
}
return {
capabilities: {
hoverProvider: true,
completionProvider: {
resolveProvider: false,
triggerCharacters: ["...", "@"],
},
definitionProvider: true,
referencesProvider: true,
documentSymbolProvider: true,
workspaceSymbolProvider: true,
codeLensProvider: {
resolveProvider: false,
},
codeActionProvider: true,
executeCommandProvider: {
commands: [],
},
textDocumentSync: TextDocumentSyncKind.Incremental,
} as ServerCapabilities,
};
});
connection.onInitialized(async () => {
initializeConnection();
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders(async (event) => {
await Promise.all([
...event.removed.map((folder) =>
workspace.removeProjectsInFolder(folder)
),
...event.added.map((folder) => workspace.addProjectsInFolder(folder)),
]);
});
}
});
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
documents.onDidChangeContent(
debounceHandler((params) => {
const project = workspace.projectForFile(params.document.uri);
if (!project) return;
project.documentDidChange(params.document);
})
);
connection.onDidChangeWatchedFiles((params) => {
for (const { uri, type } of params.changes) {
if (
uri.endsWith("apollo.config.cjs") ||
uri.endsWith("apollo.config.js") ||
uri.endsWith(".env")
) {
workspace.reloadProjectForConfig(uri);
}
// Don't respond to changes in files that are currently open,
// because we'll get content change notifications instead
if (type === FileChangeType.Changed) {
continue;
}
const project = workspace.projectForFile(uri);
if (!project) continue;
switch (type) {
case FileChangeType.Created:
project.fileDidChange(uri);
break;
case FileChangeType.Deleted:
project.fileWasDeleted(uri);
break;
}
}
});
const languageProvider = new GraphQLLanguageProvider(workspace);
connection.onHover((params, token) =>
languageProvider.provideHover(params.textDocument.uri, params.position, token)
);
connection.onDefinition((params, token) =>
languageProvider.provideDefinition(
params.textDocument.uri,
params.position,
token
)
);
connection.onReferences((params, token) =>
languageProvider.provideReferences(
params.textDocument.uri,
params.position,
params.context,
token
)
);
connection.onDocumentSymbol((params, token) =>
languageProvider.provideDocumentSymbol(params.textDocument.uri, token)
);
connection.onWorkspaceSymbol((params, token) =>
languageProvider.provideWorkspaceSymbol(params.query, token)
);
connection.onCompletion(
debounceHandler((params, token) =>
languageProvider.provideCompletionItems(
params.textDocument.uri,
params.position,
token
)
)
);
connection.onCodeLens(
debounceHandler((params, token) =>
languageProvider.provideCodeLenses(params.textDocument.uri, token)
)
);
connection.onCodeAction(
debounceHandler((params, token) =>
languageProvider.provideCodeAction(
params.textDocument.uri,
params.range,
token
)
)
);
connection.onNotification("apollographql/reloadService", () =>
workspace.reloadService()
);
connection.onNotification(
"apollographql/tagSelected",
(selection: QuickPickItem) => workspace.updateSchemaTag(selection)
);
connection.onNotification("apollographql/getStats", async ({ uri }) => {
const status = await languageProvider.provideStats(uri);
connection.sendNotification("apollographql/statsLoaded", status);
});
// Listen on the connection
connection.listen();