coc-tsserver/src/server/features/baseCodeLensProvider.ts

152 lines
4.3 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, CodeLens, Emitter, Event, Range, TextDocument } from 'vscode-languageserver-protocol'
import { CodeLensProvider } from 'coc.nvim/lib/provider'
import * as Proto from '../protocol'
import { ITypeScriptServiceClient } from '../typescriptService'
import { escapeRegExp } from '../utils/regexp'
import * as typeConverters from '../utils/typeConverters'
export class CachedNavTreeResponse {
private response?: Promise<Proto.NavTreeResponse>
private version = -1
private document = ''
public execute(document: TextDocument, f: () => Promise<Proto.NavTreeResponse>): Promise<Proto.NavTreeResponse> {
if (this.matches(document)) {
return this.response
}
return this.update(document, f())
}
private matches(document: TextDocument): boolean {
return (
this.version === document.version &&
this.document === document.uri.toString()
)
}
private update(
document: TextDocument,
response: Promise<Proto.NavTreeResponse>
): Promise<Proto.NavTreeResponse> {
this.response = response
this.version = document.version
this.document = document.uri.toString()
return response
}
}
export abstract class TypeScriptBaseCodeLensProvider implements CodeLensProvider {
private onDidChangeCodeLensesEmitter = new Emitter<void>()
public constructor(
protected client: ITypeScriptServiceClient,
private cachedResponse: CachedNavTreeResponse
) { }
public get onDidChangeCodeLenses(): Event<void> {
return this.onDidChangeCodeLensesEmitter.event
}
public async provideCodeLenses(
document: TextDocument,
token: CancellationToken
): Promise<CodeLens[]> {
const filepath = this.client.toPath(document.uri)
if (!filepath) {
return []
}
try {
const response = await this.cachedResponse.execute(document, () =>
this.client.execute('navtree', { file: filepath }, token) as any
)
if (!response) {
return []
}
const tree = response.body
const referenceableSpans: Range[] = []
if (tree && tree.childItems) {
tree.childItems.forEach(item =>
this.walkNavTree(document, item, null, referenceableSpans)
)
}
return referenceableSpans.map(
range => {
return {
range,
data: { uri: document.uri }
}
}
)
} catch {
return []
}
}
protected abstract extractSymbol(
document: TextDocument,
item: Proto.NavigationTree,
parent: Proto.NavigationTree | null
): Range | null
private walkNavTree(
document: TextDocument,
item: Proto.NavigationTree,
parent: Proto.NavigationTree | null,
results: Range[]
): void {
if (!item) {
return
}
const range = this.extractSymbol(document, item, parent)
if (range) {
results.push(range)
}
if (item.childItems) {
item.childItems.forEach(child =>
this.walkNavTree(document, child, item, results)
)
}
}
protected getSymbolRange(
document: TextDocument,
item: Proto.NavigationTree
): Range | null {
if (!item) {
return null
}
// TS 3.0+ provides a span for just the symbol
if ((item as any).nameSpan) {
return typeConverters.Range.fromTextSpan((item as any).nameSpan)
}
// In older versions, we have to calculate this manually. See #23924
const span = item.spans && item.spans[0]
if (!span) {
return null
}
const range = typeConverters.Range.fromTextSpan(span)
const text = document.getText(range)
const identifierMatch = new RegExp(
`^(.*?(\\b|\\W))${escapeRegExp(item.text || '')}(\\b|\\W)`,
'gm'
)
const match = identifierMatch.exec(text)
const prefixLength = match ? match.index + match[1].length : 0
const startOffset = document.offsetAt(range.start) + prefixLength
return {
start: document.positionAt(startOffset),
end: document.positionAt(startOffset + item.text.length)
}
}
}