Skip to content

Commit

Permalink
✨ Support IMP-Doc and access modifiers
Browse files Browse the repository at this point in the history
Resolve #1. Resolve #319.
  • Loading branch information
SPGoding committed Aug 13, 2020
1 parent 750d5d0 commit 1b3c145
Show file tree
Hide file tree
Showing 13 changed files with 340 additions and 71 deletions.
7 changes: 2 additions & 5 deletions src/data/CommandTree1.16.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2217,10 +2217,10 @@ export const CommandTree: ICommandTree = {
[Switchable]: true,
// #declare <type: string> <id: string>
'#declare': {
parser: new LiteralArgumentParser('#declare', '#define', '#register'),
parser: new LiteralArgumentParser('#declare', '#define'),
run: ({ tokens, data }) => {
const lastLiteral = getArgOrDefault(data, 1, undefined)
if (lastLiteral === '#declare' || lastLiteral === '#define' || lastLiteral === '#register') {
if (lastLiteral === '#declare' || lastLiteral === '#define') {
const lastToken = tokens[tokens.length - 1]
lastToken.range.start += 1
}
Expand Down Expand Up @@ -2250,9 +2250,6 @@ export const CommandTree: ICommandTree = {
'#define': {
redirect: 'comments.#declare'
},
'#register': {
redirect: 'comments.#declare'
},
// #alias <parser: string> <alias: string> <value: string>
'#alias': {
parser: new LiteralArgumentParser('#alias'),
Expand Down
12 changes: 8 additions & 4 deletions src/plugins/PluginLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Plugin } from '.'
import { plugins } from '..'
import { pathAccessible } from '../utils'
import { ContributorImpl } from './ContributorImpl'
import { LanguageConfig, LanguageConfigBuilderFactoryImpl } from './LanguageConfigImpl'
import { Contributions, LanguageConfig, LanguageConfigBuilderFactoryImpl } from './LanguageConfigImpl'
import { PluginID } from './Plugin'

export class PluginLoader {
Expand Down Expand Up @@ -48,17 +48,21 @@ export class PluginLoader {
return map
}

static async getContributions(plugins: Map<string, Plugin>): Promise<Map<string, LanguageConfig>> {
static async getContributions(plugins: Map<string, Plugin>): Promise<Contributions> {
const languageDefinitionContributor = new ContributorImpl<plugins.LanguageDefinition>()
const syntaxComponentContributor = new ContributorImpl<plugins.SyntaxComponentParser>()
for (const plugin of plugins.values()) {
await plugin.contributeLanguages?.(languageDefinitionContributor)
await plugin.contributeSyntaxComponentParsers?.(syntaxComponentContributor)
}
const factory = new LanguageConfigBuilderFactoryImpl({
return {
languageDefinitions: languageDefinitionContributor.values,
syntaxComponentParsers: syntaxComponentContributor.values
})
}
}

static async getLanguageConfigs(plugins: Map<string, Plugin>, contributions: Contributions): Promise<Map<string, LanguageConfig>> {
const factory = new LanguageConfigBuilderFactoryImpl(contributions)
for (const plugin of plugins.values()) {
await plugin.configureLanguages?.(factory)
}
Expand Down
265 changes: 244 additions & 21 deletions src/plugins/builtin/DocCommentPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { plugins } from '../..'
import { ArgumentNode, IdentityNode, NodeRange, NodeType } from '../../nodes'
import { combineArgumentParserResult, ParsingContext } from '../../types'
import { CompletionItemKind } from 'vscode-languageserver'
import { arrayToCompletions, arrayToMessage, plugins } from '../..'
import { locale } from '../../locales'
import { ArgumentNode, IdentityNode, NodeDescription, NodeRange, NodeType } from '../../nodes'
import { CacheType, CacheVisibility, combineArgumentParserResult, isInRange, ParsingContext, ParsingError, TextRange } from '../../types'
import { StringReader } from '../../utils/StringReader'
import { CommandSyntaxComponentParser } from './McfunctionPlugin'

export class DocCommentPlugin implements plugins.Plugin {
[plugins.PluginID] = 'spgoding:doc_comment'
Expand All @@ -20,47 +23,267 @@ export class DocCommentPlugin implements plugins.Plugin {
class DocCommentSyntaxComponentParser implements plugins.SyntaxComponentParser {
identity = 'spgoding:doc_comment/doc_comment'

static GeneralAnnotations = [
'@internal',
'@private',
'@public',
'@within'
]

static FunctionAnnotations = [
...DocCommentSyntaxComponentParser.GeneralAnnotations,
'@api',
'@context',
'@deprecated',
'@handles',
'@input',
'@output',
'@override',
'@patch',
'@reads',
'@user',
'@writes'
]

test(reader: StringReader): [boolean, number] {
const boolean = reader
.skipWhiteSpace()
.skipSpace()
.remainingString.slice(0, 2) === '#>'
return [boolean, 1]
}

parse(reader: StringReader, ctx: ParsingContext): plugins.SyntaxComponent<DocCommentNode> {
const ans = plugins.SyntaxComponent.create(this.identity, new DocCommentNode())
reader.skipWhiteSpace()
parse(reader: StringReader, ctx: ParsingContext): plugins.SyntaxComponent<DocCommentData> {
const ans = plugins.SyntaxComponent.create<DocCommentData>(this.identity, [])
reader.skipSpace()
const start = reader.cursor
const isAtFileBeginning = /^\s*$/.test(reader.passedString)
this.parseComment(ans, reader, ctx)
ans.range = { start, end: reader.cursor }
return ans
}

private parseComment(ans: plugins.SyntaxComponent<DocCommentData>, reader: StringReader, ctx: ParsingContext): void {
const start = reader.cursor
const isFunctionDoc = /^[ \t]*$/.test(reader.passedString)
const docComment = new DocCommentNode()
ans.data.push({ data: docComment })
const currentID = ctx.id?.toString()
try {
reader
.expect('#')
.skip()
.expect('>')
.skip()
.skipWhiteSpace()
if (isAtFileBeginning) {
.skipSpace()
if (isFunctionDoc) {
const idStart = reader.cursor
const idResult = new ctx.parsers
.Identity('$function', undefined, undefined, undefined, true)
.parse(reader, ctx)
ans.data.definedID = idResult.data
docComment.functionID = idResult.data
combineArgumentParserResult(ans, idResult)
const actualID = docComment.functionID.toString()
if (currentID && actualID !== currentID) {
ans.errors.push(new ParsingError(
{ start: idStart, end: reader.cursor },
locale('expected-got',
locale('punc.quote', currentID),
locale('punc.quote', actualID)
)
))
}
} else {
docComment.plainText += reader.readUntilOrEnd('\r', '\n') + '\n'
}
while (reader.jumpLine(ctx.textDoc), reader.cursor < reader.end) {
if (reader.skipSpace().peek() === '#' && StringReader.isWhiteSpace(reader.peek(1))) {
reader.skip()
// Still in the range of doc comment.
const indentStart = reader.cursor
reader.skipSpace()
if (reader.peek() === '@') {
this.parseAnnotations(docComment.annotations, reader, ctx, reader.cursor - indentStart)
} else {
docComment.plainText += reader.readUntilOrEnd('\r', '\n') + '\n'
}
} else {
if (!isFunctionDoc) {
// Attach the next command to this doc comment component.
const parser = new CommandSyntaxComponentParser()
const cmdResult = parser.parse(reader, ctx)
ans.data.push(...cmdResult.data)
combineArgumentParserResult(ans, cmdResult)
} else {
reader
.jumpLineBack(ctx.textDoc)
.readUntilOrEnd('\r', '\n')
}
break
}
}
// reader
// .jumpToNextLine(ctx.textDoc)
// .expect('#')
// .skip()
// .skipWhiteSpace()
docComment[NodeRange] = { start, end: reader.cursor }
docComment.raw = reader.string.slice(start, reader.cursor)
} catch (p) {
ans.errors.push(p)
}
ans.data[NodeRange] = { start, end: reader.cursor }
ans.range = { start, end: reader.cursor }
return ans
//#region Annotation completions.
const flattenedAnnotations = docComment.flattenedAnnotations
for (const anno of flattenedAnnotations) {
if (anno.length > 0 && isInRange(ctx.cursor, anno[0].range)) {
const pool = isFunctionDoc ? DocCommentSyntaxComponentParser.FunctionAnnotations : DocCommentSyntaxComponentParser.GeneralAnnotations
ans.completions.push(...arrayToCompletions(
pool, anno[0].range.start, anno[0].range.end, v => ({ ...v, kind: CompletionItemKind.Keyword })
))
break
}
}
//#endregion
const visibility = this.getVisibility(ans, flattenedAnnotations, currentID, ctx)
this.setCache(ans, visibility, docComment)
}

private parseAnnotations(annotations: Annotation[], reader: StringReader, ctx: ParsingContext, indent: number): void {
const start = reader.cursor
const value = reader.readUntilOrEnd(' ', '\r', '\n')
const anno: Annotation = { value: { range: { start, end: reader.cursor }, raw: value } }
if (reader.peek() === ' ') {
reader.skipSpace()
this.parseAnnotations(anno.children = anno.children ?? [], reader, ctx, indent)
} else {
while (true) {
const clonedReader = reader
.clone()
.jumpLine(ctx.textDoc)
.skipSpace()
if (clonedReader.peek() === '#') {
clonedReader.skip()
const indentStart = clonedReader.cursor
clonedReader.skipSpace()
const nextIndent = clonedReader.cursor - indentStart
if (nextIndent - indent >= 2) {
reader.cursor = clonedReader.cursor
this.parseAnnotations(anno.children = anno.children ?? [], reader, ctx, nextIndent)
continue
}
}
break
}
}
annotations.push(anno)
}

private getVisibility(ans: plugins.SyntaxComponent<DocCommentData>, flattenedAnnotations: AnnotationValue[][], currentID: string | undefined, ctx: ParsingContext) {
const visibilities: CacheVisibility[] = []
for (const anno of flattenedAnnotations) {
if (anno.length > 0) {
switch (anno[0].raw) {
case '@private':
if (currentID) {
visibilities.push({ type: 'function', pattern: currentID })
}
break
case '@within':
if (anno.length === 2) {
visibilities.push({ type: '*', pattern: anno[1].raw })
} else if (anno.length >= 3) {
if (anno[1].raw === 'advancement' || anno[1].raw === 'function' || anno[1].raw === 'tag/function' || anno[1].raw === '*') {
visibilities.push({ type: anno[1].raw, pattern: anno[2].raw })
} else {
ans.errors.push(new ParsingError(
anno[1].range,
locale('expected-got',
arrayToMessage(['advancement', 'function', 'tag/function'], true, 'or'),
locale('punc.quote', anno[1].raw)
)
))
}
} else {
ans.errors.push(new ParsingError(
anno[0].range,
locale('expected-got',
locale('more-arguments'),
locale('nothing')
)
))
}
break
case '@internal':
if (currentID) {
visibilities.push({ type: '*', pattern: `${ctx.id!.getNamespace()}:**` })
}
break
case '@public':
case '@api':
visibilities.push({ type: '*', pattern: '**' })
break
default:
break
}
}
}
/* DEBUG */ console.log('getVisibility', require('util').inspect(visibilities, true, null))
return visibilities
}

private setCache(ans: plugins.SyntaxComponent<DocCommentData>, visibility: CacheVisibility[], docComment: DocCommentNode) {
for (const type of Object.keys(ans.cache)) {
for (const id of Object.keys(ans.cache[type as CacheType]!)) {
const unit = ans.cache[type as CacheType]![id]!
unit.doc = docComment[NodeDescription]
for (const pos of [...unit.dcl ?? [], ...unit.def ?? []]) {
pos.visibility = visibility
}
}
}
}
}

type DocCommentData = { data: ArgumentNode }[]

type AnnotationValue = { raw: string, range: TextRange }
type Annotation = {
value: AnnotationValue,
children?: Annotation[]
}

class DocCommentNode extends ArgumentNode {
[NodeType]: 'builtin:doc_comment'
definedID: IdentityNode
[NodeType] = 'builtin:doc_comment'

raw = ''

plainText = ''
annotations: Annotation[] = []

functionID: IdentityNode | undefined = undefined

set [NodeDescription](_: string) { }
get [NodeDescription]() {
return this.valueOf()
}

get flattenedAnnotations() {
return DocCommentNode.flattenAnnotations(this.annotations)
}

toString() {
return this.raw
}

valueOf() {
return this.plainText + '\n\n' +
this.flattenedAnnotations
.map(values => values.map(v => v.raw).join(' '))
.join('\n\n')
}

static flattenAnnotations(annotations: Annotation[], prefix: AnnotationValue[] = []) {
const ans: AnnotationValue[][] = []
for (const anno of annotations) {
if (anno.children) {
ans.push(...this.flattenAnnotations(anno.children, [...prefix, anno.value]))
} else {
ans.push([...prefix, anno.value])
}
}
return ans
}
}
2 changes: 1 addition & 1 deletion src/plugins/builtin/McfunctionPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class McfunctionPlugin implements plugins.Plugin {
}
}

class CommandSyntaxComponentParser implements plugins.SyntaxComponentParser<CommandComponentData> {
export class CommandSyntaxComponentParser implements plugins.SyntaxComponentParser<CommandComponentData> {
identity = 'spgoding:mcfunction/command'

test(): [boolean, number] {
Expand Down
Loading

0 comments on commit 1b3c145

Please sign in to comment.