mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
290 lines
10 KiB
TypeScript
290 lines
10 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License;
|
|
* you may not use this file except in compliance with the Elastic License.
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import util from 'util';
|
|
|
|
import { ResponseError } from 'vscode-jsonrpc';
|
|
|
|
import { ProgressReporter } from '.';
|
|
import { TEXT_FILE_LIMIT } from '../../common/file';
|
|
import {
|
|
LanguageServerNotInstalled,
|
|
LanguageServerStartFailed,
|
|
} from '../../common/lsp_error_codes';
|
|
import { toCanonicalUrl } from '../../common/uri_util';
|
|
import { Document, IndexStats, IndexStatsKey, LspIndexRequest, RepositoryUri } from '../../model';
|
|
import { GitOperations, HEAD } from '../git_operations';
|
|
import { EsClient } from '../lib/esqueue';
|
|
import { Logger } from '../log';
|
|
import { LspService } from '../lsp/lsp_service';
|
|
import { ServerOptions } from '../server_options';
|
|
import { detectLanguage, detectLanguageByFilename } from '../utils/detect_language';
|
|
import { AbstractIndexer } from './abstract_indexer';
|
|
import { BatchIndexHelper } from './batch_index_helper';
|
|
import {
|
|
getDocumentIndexCreationRequest,
|
|
getReferenceIndexCreationRequest,
|
|
getSymbolIndexCreationRequest,
|
|
} from './index_creation_request';
|
|
import { ALL_RESERVED, DocumentIndexName, ReferenceIndexName, SymbolIndexName } from './schema';
|
|
|
|
export class LspIndexer extends AbstractIndexer {
|
|
protected type: string = 'lsp';
|
|
// Batch index helper for symbols/references
|
|
protected lspBatchIndexHelper: BatchIndexHelper;
|
|
// Batch index helper for documents
|
|
protected docBatchIndexHelper: BatchIndexHelper;
|
|
|
|
private LSP_BATCH_INDEX_SIZE = 1000;
|
|
private DOC_BATCH_INDEX_SIZE = 50;
|
|
|
|
constructor(
|
|
protected readonly repoUri: RepositoryUri,
|
|
protected readonly revision: string,
|
|
protected readonly lspService: LspService,
|
|
protected readonly options: ServerOptions,
|
|
protected readonly gitOps: GitOperations,
|
|
protected readonly client: EsClient,
|
|
protected readonly log: Logger
|
|
) {
|
|
super(repoUri, revision, client, log);
|
|
|
|
this.lspBatchIndexHelper = new BatchIndexHelper(client, log, this.LSP_BATCH_INDEX_SIZE);
|
|
this.docBatchIndexHelper = new BatchIndexHelper(client, log, this.DOC_BATCH_INDEX_SIZE);
|
|
}
|
|
|
|
public async start(progressReporter?: ProgressReporter, checkpointReq?: LspIndexRequest) {
|
|
try {
|
|
return await super.start(progressReporter, checkpointReq);
|
|
} finally {
|
|
if (!this.isCancelled()) {
|
|
// Flush all the index request still in the cache for bulk index.
|
|
this.lspBatchIndexHelper.flush();
|
|
this.docBatchIndexHelper.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
public cancel() {
|
|
this.lspBatchIndexHelper.cancel();
|
|
this.docBatchIndexHelper.cancel();
|
|
super.cancel();
|
|
}
|
|
|
|
// If the current checkpoint is valid
|
|
protected validateCheckpoint(checkpointReq?: LspIndexRequest): boolean {
|
|
return checkpointReq !== undefined && checkpointReq.revision === this.revision;
|
|
}
|
|
|
|
// If it's necessary to refresh (create and reset) all the related indices
|
|
protected needRefreshIndices(checkpointReq?: LspIndexRequest): boolean {
|
|
// If it's not resumed from a checkpoint, then try to refresh all the indices.
|
|
return !this.validateCheckpoint(checkpointReq);
|
|
}
|
|
|
|
protected ifCheckpointMet(req: LspIndexRequest, checkpointReq: LspIndexRequest): boolean {
|
|
// Assume for the same revision, the order of the files we iterate the repository is definite
|
|
// everytime.
|
|
return req.filePath === checkpointReq.filePath && req.revision === checkpointReq.revision;
|
|
}
|
|
|
|
protected async prepareIndexCreationRequests() {
|
|
return [
|
|
getDocumentIndexCreationRequest(this.repoUri),
|
|
getReferenceIndexCreationRequest(this.repoUri),
|
|
getSymbolIndexCreationRequest(this.repoUri),
|
|
];
|
|
}
|
|
|
|
protected async *getIndexRequestIterator(): AsyncIterableIterator<LspIndexRequest> {
|
|
let repo;
|
|
try {
|
|
const { workspaceRepo } = await this.lspService.workspaceHandler.openWorkspace(
|
|
this.repoUri,
|
|
HEAD
|
|
);
|
|
repo = workspaceRepo;
|
|
const workspaceDir = workspaceRepo.workdir();
|
|
const fileIterator = await this.gitOps.iterateRepo(this.repoUri, HEAD);
|
|
for await (const file of fileIterator) {
|
|
const filePath = file.path!;
|
|
const req: LspIndexRequest = {
|
|
repoUri: this.repoUri,
|
|
localRepoPath: workspaceDir,
|
|
filePath,
|
|
// Always use HEAD for now until we have multi revision.
|
|
// Also, since the workspace might get updated during the index, we always
|
|
// want the revision to keep updated so that lsp proxy could pass the revision
|
|
// check per discussion here: https://github.com/elastic/code/issues/1317#issuecomment-504615833
|
|
revision: HEAD,
|
|
};
|
|
yield req;
|
|
}
|
|
} catch (error) {
|
|
this.log.error(`Prepare lsp indexing requests error.`);
|
|
this.log.error(error);
|
|
throw error;
|
|
} finally {
|
|
if (repo) {
|
|
repo.cleanup();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected async getIndexRequestCount(): Promise<number> {
|
|
try {
|
|
return await this.gitOps.countRepoFiles(this.repoUri, HEAD);
|
|
} catch (error) {
|
|
if (this.isCancelled()) {
|
|
this.log.debug(`Indexer got cancelled. Skip get index count error.`);
|
|
return 1;
|
|
} else {
|
|
this.log.error(`Get lsp index requests count error.`);
|
|
this.log.error(error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected async cleanIndex() {
|
|
// Clean up all the symbol documents in the symbol index
|
|
try {
|
|
await this.client.deleteByQuery({
|
|
index: SymbolIndexName(this.repoUri),
|
|
body: {
|
|
query: {
|
|
match_all: {},
|
|
},
|
|
},
|
|
});
|
|
this.log.info(`Clean up symbols for ${this.repoUri} done.`);
|
|
} catch (error) {
|
|
this.log.error(`Clean up symbols for ${this.repoUri} error.`);
|
|
this.log.error(error);
|
|
}
|
|
|
|
// Clean up all the reference documents in the reference index
|
|
try {
|
|
await this.client.deleteByQuery({
|
|
index: ReferenceIndexName(this.repoUri),
|
|
body: {
|
|
query: {
|
|
match_all: {},
|
|
},
|
|
},
|
|
});
|
|
this.log.info(`Clean up references for ${this.repoUri} done.`);
|
|
} catch (error) {
|
|
this.log.error(`Clean up references for ${this.repoUri} error.`);
|
|
this.log.error(error);
|
|
}
|
|
|
|
// Clean up all the document documents in the document index but keep the repository document.
|
|
try {
|
|
await this.client.deleteByQuery({
|
|
index: DocumentIndexName(this.repoUri),
|
|
body: {
|
|
query: {
|
|
bool: {
|
|
must_not: ALL_RESERVED.map((field: string) => ({
|
|
exists: {
|
|
field,
|
|
},
|
|
})),
|
|
},
|
|
},
|
|
},
|
|
});
|
|
this.log.info(`Clean up documents for ${this.repoUri} done.`);
|
|
} catch (error) {
|
|
this.log.error(`Clean up documents for ${this.repoUri} error.`);
|
|
this.log.error(error);
|
|
}
|
|
}
|
|
|
|
protected async processRequest(request: LspIndexRequest): Promise<IndexStats> {
|
|
const stats: IndexStats = new Map<IndexStatsKey, number>()
|
|
.set(IndexStatsKey.Symbol, 0)
|
|
.set(IndexStatsKey.Reference, 0)
|
|
.set(IndexStatsKey.File, 0);
|
|
const { repoUri, revision, filePath, localRepoPath } = request;
|
|
|
|
this.log.debug(`Indexing ${filePath} at revision ${revision} for ${repoUri}`);
|
|
const lspDocUri = toCanonicalUrl({ repoUri, revision, file: filePath, schema: 'git:' });
|
|
const symbolNames = new Set<string>();
|
|
|
|
const localFilePath = `${localRepoPath}${filePath}`;
|
|
const lstat = util.promisify(fs.lstat);
|
|
const stat = await lstat(localFilePath);
|
|
|
|
if (stat.size > TEXT_FILE_LIMIT) {
|
|
this.log.debug(`File size exceeds limit. Skip index.`);
|
|
return stats;
|
|
}
|
|
|
|
const readLink = util.promisify(fs.readlink);
|
|
const readFile = util.promisify(fs.readFile);
|
|
const content = stat.isSymbolicLink()
|
|
? await readLink(localFilePath, 'utf8')
|
|
: await readFile(localFilePath, 'utf8');
|
|
|
|
try {
|
|
const lang = detectLanguageByFilename(filePath);
|
|
// filter file by language
|
|
if (lang && this.lspService.supportLanguage(lang)) {
|
|
const response = await this.lspService.sendRequest('textDocument/full', {
|
|
textDocument: {
|
|
uri: lspDocUri,
|
|
},
|
|
reference: this.options.enableGlobalReference,
|
|
});
|
|
|
|
if (response && response.result && response.result.length > 0 && response.result[0]) {
|
|
const { symbols, references } = response.result[0];
|
|
for (const symbol of symbols) {
|
|
await this.lspBatchIndexHelper.index(SymbolIndexName(repoUri), symbol);
|
|
symbolNames.add(symbol.symbolInformation.name);
|
|
}
|
|
stats.set(IndexStatsKey.Symbol, symbols.length);
|
|
|
|
for (const ref of references) {
|
|
await this.lspBatchIndexHelper.index(ReferenceIndexName(repoUri), ref);
|
|
}
|
|
stats.set(IndexStatsKey.Reference, references.length);
|
|
} else {
|
|
this.log.debug(`Empty response from lsp server. Skip symbols and references indexing.`);
|
|
}
|
|
} else {
|
|
this.log.debug(`Unsupported language. Skip symbols and references indexing.`);
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof ResponseError && error.code === LanguageServerNotInstalled) {
|
|
// TODO maybe need to report errors to the index task and warn user later
|
|
this.log.debug(`Index symbols or references error due to language server not installed`);
|
|
} else if (error instanceof ResponseError && error.code === LanguageServerStartFailed) {
|
|
this.log.debug(
|
|
`Index symbols or references error due to language server can't be started.`
|
|
);
|
|
} else {
|
|
this.log.warn(`Index symbols or references error.`);
|
|
this.log.warn(error);
|
|
}
|
|
}
|
|
|
|
const language = await detectLanguage(filePath, Buffer.from(content));
|
|
const body: Document = {
|
|
repoUri,
|
|
path: filePath,
|
|
content,
|
|
language,
|
|
qnames: Array.from(symbolNames),
|
|
};
|
|
await this.docBatchIndexHelper.index(DocumentIndexName(repoUri), body);
|
|
stats.set(IndexStatsKey.File, 1);
|
|
return stats;
|
|
}
|
|
}
|