Skip to content

Commit

Permalink
Add code action to create undeclared symbol
Browse files Browse the repository at this point in the history
  • Loading branch information
misode committed Dec 30, 2024
1 parent a5c7bca commit 9a78ac3
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 2 deletions.
17 changes: 16 additions & 1 deletion packages/core/src/processor/linter/builtin/undeclaredSymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ export const undeclaredSymbol: Linter<AstNode> = (node, ctx) => {
})
}
if (Config.Action.isReport(action)) {
const info: LanguageErrorInfo = {}
const uriBuilder = ctx.meta.getUriBuilder(node.symbol.category)
if (uriBuilder) {
const uri = uriBuilder(node.symbol.identifier, ctx)
if (uri) {
info.codeAction = {
title: localize(
'code-action.create-undeclared-file',
node.symbol.category,
localeQuote(node.symbol.identifier),
),
changes: [{ type: 'create', uri }],
}
}
}
const severityOverride = action.report === 'inherit'
? undefined
: LinterSeverity.toErrorSeverity(action.report)
Expand All @@ -31,7 +46,7 @@ export const undeclaredSymbol: Linter<AstNode> = (node, ctx) => {
localeQuote(node.symbol.identifier),
),
node,
undefined,
info,
severityOverride,
)
}
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/service/MetaRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type { SignatureHelpProvider } from '../processor/SignatureHelpProvider.j
import type { DependencyKey, DependencyProvider } from './Dependency.js'
import type { FileExtension } from './fileUtil.js'
import type { SymbolRegistrar } from './SymbolRegistrar.js'
import type { UriBuilder } from './UriBuilder.js'
import type { UriBinder, UriSorter, UriSorterRegistration } from './UriProcessor.js'

export interface LanguageOptions {
Expand Down Expand Up @@ -76,6 +77,7 @@ export class MetaRegistry {
readonly #symbolRegistrars = new Map<string, SymbolRegistrarRegistration>()
readonly #custom = new Map<string, Map<string, unknown>>()
readonly #uriBinders = new Set<UriBinder>()
readonly #uriBuilders = new Map<string, UriBuilder>()
#uriSorter: UriSorter = () => 0

constructor() {
Expand Down Expand Up @@ -300,6 +302,16 @@ export class MetaRegistry {
return this.#uriBinders
}

public hasUriBuilder(category: string): boolean {
return this.#uriBuilders.has(category)
}
public getUriBuilder(category: string): UriBuilder | undefined {
return this.#uriBuilders.get(category)
}
public registerUriBuilder(category: string, builder: UriBuilder): void {
this.#uriBuilders.set(category, builder)
}

public setUriSorter(uriSorter: UriSorterRegistration): void {
const nextSorter = this.#uriSorter
this.#uriSorter = (a, b) => uriSorter(a, b, nextSorter)
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/service/UriBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { TextDocument } from 'vscode-languageserver-textdocument'
import type { RootUriString } from '../service/index.js'
import type { Config } from './Config.js'

export type UriBuilder = (identifier: string, ctx: UriBuilderContext) => string | undefined

export interface UriBuilderContext {
doc: TextDocument
project: Record<string, string>
roots: readonly RootUriString[]
config: Config
}
17 changes: 17 additions & 0 deletions packages/core/src/service/fileUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ export namespace fileUtil {
return getRels(uri, rootUris).next().value
}

export function* getRoots(
uri: string,
rootUris: readonly RootUriString[],
): Generator<RootUriString, undefined, unknown> {
for (const root of rootUris) {
const rel = getRelativeUriFromBase(uri, root)
if (rel !== undefined) {
yield root
}
}
return undefined
}

export function getRoot(uri: string, rootUris: readonly RootUriString[]): string | undefined {
return getRoots(uri, rootUris).next().value
}

export function isRootUri(uri: string): uri is RootUriString {
return uri.endsWith('/')
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export * from './Project.js'
export * from './Service.js'
export * from './SymbolLocations.js'
export * from './SymbolRegistrar.js'
export * from './UriBuilder.js'
export * from './UriProcessor.js'
3 changes: 3 additions & 0 deletions packages/core/src/source/LanguageError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,7 @@ export type CodeActionChange = {
type: 'edit'
range: Range
text: string
} | {
type: 'create'
uri: string
}
59 changes: 59 additions & 0 deletions packages/java-edition/src/binder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import type {
CheckerContext,
Config,
FileCategory,
MetaRegistry,
RootUriString,
TaggableResourceLocationCategory,
UriBinder,
UriBinderContext,
UriBuilder,
} from '@spyglassmc/core'
import {
ErrorSeverity,
Expand Down Expand Up @@ -163,6 +165,13 @@ resource('', {
})
resource('', { pack: 'assets', category: 'gpu_warnlist', identifier: 'gpu_warnlist' })

export function* getResources() {
for (const resources of Resources.values()) {
yield* resources
}
return undefined
}

export function* getRels(
uri: string,
rootUris: readonly RootUriString[],
Expand All @@ -179,6 +188,22 @@ export function* getRels(
return undefined
}

export function* getRoots(
uri: string,
rootUris: readonly RootUriString[],
): Generator<RootUriString, undefined, unknown> {
yield* fileUtil.getRoots(uri, rootUris)

const parts = uri.split('/')
for (let i = parts.length - 2; i >= 0; i--) {
if (parts[i] === 'data' || parts[i] === 'assets') {
yield `${parts.slice(0, i).join('/')}/`
}
}

return undefined
}

export function dissectUri(uri: string, ctx: UriBinderContext) {
const rels = getRels(uri, ctx.roots)
const release = ctx.project['loadedVersion'] as ReleaseVersion | undefined
Expand Down Expand Up @@ -321,3 +346,37 @@ export function reportDissectError(
)
}
}

function uriBuilder(resources: Resource[]): UriBuilder {
return (identifier, ctx) => {
const root = getRoots(ctx.doc.uri, ctx.roots).next().value
if (!root) {
return undefined
}
const release = ctx.project['loadedVersion'] as ReleaseVersion | undefined
if (!release) {
return undefined
}
const resource = resources.find(r => matchVersion(release, r.since, r.until))
if (!resource) {
return undefined
}
const sepIndex = identifier.indexOf(':')
const namespace = sepIndex > 0 ? identifier.slice(0, sepIndex) : 'minecraft'
const path = identifier.slice(sepIndex + 1)
return `${root}${resource.pack}/${namespace}/${resource.path}/${path}${resource.ext}`
}
}

export function registerUriBuilders(meta: MetaRegistry) {
const resourcesByCategory = new Map<string, Resource[]>()
for (const resource of getResources()) {
resourcesByCategory.set(resource.category, [
...resourcesByCategory.get(resource.category) ?? [],
resource,
])
}
for (const [category, resources] of resourcesByCategory.entries()) {
meta.registerUriBuilder(category, uriBuilder(resources))
}
}
3 changes: 2 additions & 1 deletion packages/java-edition/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as core from '@spyglassmc/core'
import * as json from '@spyglassmc/json'
import * as mcdoc from '@spyglassmc/mcdoc'
import * as nbt from '@spyglassmc/nbt'
import { uriBinder } from './binder/index.js'
import { registerUriBuilders, uriBinder } from './binder/index.js'
import type { McmetaSummary, McmetaVersions, PackInfo } from './dependency/index.js'
import {
getMcmetaSummary,
Expand Down Expand Up @@ -72,6 +72,7 @@ export const initialize: core.ProjectInitializer = async (ctx) => {
}

meta.registerUriBinder(uriBinder)
registerUriBuilders(meta)

const versions = await getVersions(ctx.externals, ctx.downloader)
if (!versions) {
Expand Down
5 changes: 5 additions & 0 deletions packages/language-server/src/util/toLS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ export function codeAction(codeAction: core.CodeAction, doc: TextDocument): ls.C
textDocument: { uri: doc.uri, version: doc.version },
edits: [{ range: range(change.range, doc), newText: change.text }],
} satisfies ls.TextDocumentEdit
case 'create':
return {
kind: 'create',
uri: change.uri,
} satisfies ls.CreateFile
}
}),
}
Expand Down

0 comments on commit 9a78ac3

Please sign in to comment.