Skip to content

Commit

Permalink
Make some readability improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
wolmir committed Oct 3, 2022
1 parent 8d76972 commit 51bd789
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 201 deletions.
30 changes: 1 addition & 29 deletions extension/src/lspClient/languageClient.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Uri, workspace } from 'vscode'
import {
LanguageClient,
LanguageClientOptions,
ServerOptions,
TransportKind
} from 'vscode-languageclient/node'
import { documentSelector, serverModule } from 'dvc-vscode-lsp'
import { readFileSync } from 'fs-extra'
import { Disposable } from '../class/dispose'
import { findFiles } from '../fileSystem/workspace'

export class LanguageClientWrapper extends Disposable {
private client: LanguageClient
Expand All @@ -17,13 +14,7 @@ export class LanguageClientWrapper extends Disposable {
super()

const clientOptions: LanguageClientOptions = {
documentSelector,

synchronize: {
fileEvents: workspace.createFileSystemWatcher(
'**/*.{yaml,dvc,dvc.lock,json,toml}'
)
}
documentSelector
}

this.client = this.dispose.track(
Expand All @@ -42,25 +33,6 @@ export class LanguageClientWrapper extends Disposable {
async start() {
await this.client.start()

const files = await findFiles('**/*.{yaml,json,py,toml}', '.??*')

const textDocuments = files.map(filePath => {
const uri = Uri.file(filePath).toString()
const languageId = filePath.endsWith('yaml') ? 'yaml' : 'json'
const text = readFileSync(filePath, 'utf8')

return {
languageId,
text,
uri,
version: 0
}
})

await this.client.sendRequest('initialTextDocuments', {
textDocuments
})

return this
}

Expand Down
151 changes: 9 additions & 142 deletions languageServer/src/TextDocumentWrapper.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
import {
DocumentSymbol,
Position,
SymbolKind,
Range,
Location
} from 'vscode-languageserver/node'
import { DocumentSymbol, Position, Location } from 'vscode-languageserver/node'
import { TextDocument } from 'vscode-languageserver-textdocument'
import {
isNode,
isScalar,
parseDocument,
visit,
Node,
Scalar,
Pair,
isPair
} from 'yaml'
import { alphadecimalWords, variableTemplates } from './regexes'
import { parseDocument } from 'yaml'
import { ITextDocumentWrapper } from './ITextDocumentWrapper'
import { LanguageHelper } from './languageHelpers/baseLanguageHelper'
import { createLanguageHelper } from './languageHelpers'

export class TextDocumentWrapper implements ITextDocumentWrapper {
uri: string

private textDocument: TextDocument
private languageHelper: LanguageHelper

constructor(textDocument: TextDocument) {
this.textDocument = textDocument
this.uri = this.textDocument.uri
this.languageHelper = createLanguageHelper(this.textDocument)
}

public offsetAt(position: Position) {
Expand All @@ -45,132 +33,11 @@ export class TextDocumentWrapper implements ITextDocumentWrapper {
return parseDocument(this.getText())
}

public findLocationsFor(aSymbol: DocumentSymbol) {
const parts = aSymbol.name.split(/\s/g)
const txt = this.getText()

const acc: Location[] = []
for (const str of parts) {
const index = txt.indexOf(str)
if (index <= 0) {
continue
}
const pos = this.positionAt(index)
const range = this.symbolAt(pos)?.range
if (!range) {
continue
}
acc.push(Location.create(this.uri, range as Range))
}
return acc
public findLocationsFor(symbol: DocumentSymbol): Location[] {
return this.languageHelper.findLocationsFor(symbol)
}

public symbolAt(position: Position): DocumentSymbol | undefined {
return this.symbolScopeAt(position).pop()
}

private getTemplateExpressionSymbolsInsideScalar(
scalarValue: string,
nodeOffset: number
) {
const templateSymbols: DocumentSymbol[] = []

const templates = scalarValue.matchAll(variableTemplates)
for (const template of templates) {
const expression = template[1]
const expressionOffset: number = nodeOffset + (template.index ?? 0) + 2 // To account for the '${'
const symbols = expression.matchAll(alphadecimalWords) // It works well for now. We can always add more sophistication when needed.

for (const templateSymbol of symbols) {
const symbolStart = (templateSymbol.index ?? 0) + expressionOffset
const symbolEnd = symbolStart + templateSymbol[0].length
const symbolRange = Range.create(
this.positionAt(symbolStart),
this.positionAt(symbolEnd)
)
templateSymbols.push(
DocumentSymbol.create(
templateSymbol[0],
undefined,
SymbolKind.Variable,
symbolRange,
symbolRange
)
)
}
}

return templateSymbols
}

private yamlScalarNodeToDocumentSymbols(
node: Scalar,
[nodeStart, valueEnd, nodeEnd]: [number, number, number]
) {
const nodeValue = `${node.value}`

let symbolKind: SymbolKind = SymbolKind.String

if (/\.[A-Za-z]+$/.test(nodeValue)) {
symbolKind = SymbolKind.File
}

const symbolsSoFar: DocumentSymbol[] = [
DocumentSymbol.create(
nodeValue,
undefined,
symbolKind,
Range.create(this.positionAt(nodeStart), this.positionAt(nodeEnd)),
Range.create(this.positionAt(nodeStart), this.positionAt(valueEnd))
),
...this.getTemplateExpressionSymbolsInsideScalar(nodeValue, nodeStart)
]

return symbolsSoFar
}

private yamlNodeToDocumentSymbols(
node: Node | Pair,
range: [number, number, number]
): DocumentSymbol[] {
if (isScalar(node)) {
return this.yamlScalarNodeToDocumentSymbols(node, range)
}

if (isPair(node)) {
return this.yamlNodeToDocumentSymbols(node.value as Node | Pair, range)
}

return []
}

private symbolScopeAt(position: Position): DocumentSymbol[] {
const cursorOffset: number = this.offsetAt(position)

const symbolsFound: Array<DocumentSymbol | null> = []

visit(this.getYamlDocument(), (_, node) => {
if (isNode(node) && node.range) {
const range = node.range
const nodeStart = range[0]
const nodeEnd = range[2]
const isCursorInsideNode =
cursorOffset >= nodeStart && cursorOffset <= nodeEnd

if (isCursorInsideNode) {
symbolsFound.push(...this.yamlNodeToDocumentSymbols(node, range))
}
}
})
const symbolStack = (symbolsFound.filter(Boolean) as DocumentSymbol[]).sort(
(a, b) => {
const offA = this.offsetAt(a.range.end) - this.offsetAt(a.range.start)
const offB = this.offsetAt(b.range.end) - this.offsetAt(b.range.start)

return offB - offA // We want the tighter fits for last, so we can just pop them
}
)

return [...symbolStack]
return this.languageHelper.findSymbolAtPosition(position)
}
}
105 changes: 105 additions & 0 deletions languageServer/src/languageHelpers/baseLanguageHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { has } from 'lodash'
import { TextDocument } from 'vscode-languageserver-textdocument'
import {
DocumentSymbol,
Position,
Location,
SymbolKind
} from 'vscode-languageserver/node'

export interface LanguageHelper {
findSymbolAtPosition(position: Position): DocumentSymbol | undefined
findLocationsFor(symbol: DocumentSymbol): Location[]
}

export abstract class BaseLanguageHelper<RootNode> implements LanguageHelper {
protected textDocument: TextDocument
protected rootNode?: RootNode

constructor(textDocument: TextDocument) {
this.textDocument = textDocument
this.rootNode = this.parse(this.getText())
}

public findSymbolAtPosition(position: Position): DocumentSymbol | undefined {
const cursorOffset: number = this.offsetAt(position)
const symbolsAroundOffset = this.findEnclosingSymbols(cursorOffset)

const symbolStack = symbolsAroundOffset.sort((a, b) => {
const offA = this.offsetAt(a.range.end) - this.offsetAt(a.range.start)
const offB = this.offsetAt(b.range.end) - this.offsetAt(b.range.start)

return offB - offA // We want the tighter fits for last, so we can just pop them
})

return [...symbolStack].pop()
}

public findLocationsFor(symbol: DocumentSymbol): Location[] {
if (symbol.kind === SymbolKind.Property) {
return this.findLocationsForPropertySymbol(symbol)
}

return this.findLocationsForNormalSymbol(symbol)
}

protected getText() {
return this.textDocument.getText()
}

protected offsetAt(position: Position) {
return this.textDocument.offsetAt(position)
}

protected positionAt(offset: number) {
return this.textDocument.positionAt(offset)
}

private findLocationsForPropertySymbol(symbol: DocumentSymbol) {
const propertyPath = symbol.detail
const itIsHere = propertyPath && this.hasProperty(propertyPath)

if (itIsHere) {
const pathArray = propertyPath.split('.')
const location = this.getPropertyLocation(pathArray)

return location ? [location] : []
}

return this.findLocationsForNormalSymbol(symbol)
}

private findLocationsForNormalSymbol(symbol: DocumentSymbol) {
const parts = symbol.name.split(/\s/g)
const txt = this.getText()

const acc: Location[] = []
for (const str of parts) {
const index = txt.indexOf(str)
if (index <= 0) {
continue
}
const pos = this.positionAt(index)
const range = this.findSymbolAtPosition(pos)?.range

if (range) {
acc.push(Location.create(this.textDocument.uri, range))
}
}
return acc
}

private hasProperty(path: string) {
const parsedObj = this.toJSON()

return has(parsedObj, path)
}

protected abstract parse(source: string): RootNode | undefined
protected abstract findEnclosingSymbols(offset: number): DocumentSymbol[]
protected abstract getPropertyLocation(
pathArray: Array<string | number>
): Location | null

protected abstract toJSON(): unknown
}
18 changes: 18 additions & 0 deletions languageServer/src/languageHelpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { TextDocument } from 'vscode-languageserver-textdocument'
import { JsonHelper } from './jsonHelper'
import { PlainTextHelper } from './plainTextHelper'
import { YamlHelper } from './yamlHelper'

export const createLanguageHelper = (textDocument: TextDocument) => {
const language = textDocument.languageId

if (language === 'yaml') {
return new YamlHelper(textDocument)
}

if (language === 'json') {
return new JsonHelper(textDocument)
}

return new PlainTextHelper(textDocument)
}
34 changes: 34 additions & 0 deletions languageServer/src/languageHelpers/jsonHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { findNodeAtLocation, getNodeValue, Node, parseTree } from 'jsonc-parser'
import { DocumentSymbol, Location, Range } from 'vscode-languageserver'
import { BaseLanguageHelper } from './baseLanguageHelper'

export class JsonHelper extends BaseLanguageHelper<Node> {
protected parse(source: string) {
return parseTree(source)
}

protected findEnclosingSymbols(): DocumentSymbol[] {
return []
}

protected getPropertyLocation(
pathArray: Array<string | number>
): Location | null {
const node = this.rootNode && findNodeAtLocation(this.rootNode, pathArray)

if (!node) {
return null
}
const nodeSrcIndex = node.offset
const nodeSrcLength = node.length
const nodeEnd = nodeSrcIndex + nodeSrcLength
const start = this.positionAt(nodeSrcIndex)
const end = this.positionAt(nodeEnd)
const range = Range.create(start, end)
return Location.create(this.textDocument.uri, range)
}

protected toJSON(): unknown {
return this.rootNode ? getNodeValue(this.rootNode) : undefined
}
}
Loading

0 comments on commit 51bd789

Please sign in to comment.