diff --git a/packages/core/src/processor/linter/builtin/undeclaredSymbol.ts b/packages/core/src/processor/linter/builtin/undeclaredSymbol.ts index c83c2139e..47ef90c65 100644 --- a/packages/core/src/processor/linter/builtin/undeclaredSymbol.ts +++ b/packages/core/src/processor/linter/builtin/undeclaredSymbol.ts @@ -21,6 +21,21 @@ export const undeclaredSymbol: Linter = (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) @@ -31,7 +46,7 @@ export const undeclaredSymbol: Linter = (node, ctx) => { localeQuote(node.symbol.identifier), ), node, - undefined, + info, severityOverride, ) } diff --git a/packages/core/src/service/MetaRegistry.ts b/packages/core/src/service/MetaRegistry.ts index cfc1b45c4..109c44040 100644 --- a/packages/core/src/service/MetaRegistry.ts +++ b/packages/core/src/service/MetaRegistry.ts @@ -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 { @@ -76,6 +77,7 @@ export class MetaRegistry { readonly #symbolRegistrars = new Map() readonly #custom = new Map>() readonly #uriBinders = new Set() + readonly #uriBuilders = new Map() #uriSorter: UriSorter = () => 0 constructor() { @@ -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) diff --git a/packages/core/src/service/UriBuilder.ts b/packages/core/src/service/UriBuilder.ts new file mode 100644 index 000000000..d6055da8e --- /dev/null +++ b/packages/core/src/service/UriBuilder.ts @@ -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 + roots: readonly RootUriString[] + config: Config +} diff --git a/packages/core/src/service/fileUtil.ts b/packages/core/src/service/fileUtil.ts index b028212ba..3dd0253d0 100644 --- a/packages/core/src/service/fileUtil.ts +++ b/packages/core/src/service/fileUtil.ts @@ -76,6 +76,23 @@ export namespace fileUtil { return getRels(uri, rootUris).next().value } + export function* getRoots( + uri: string, + rootUris: readonly RootUriString[], + ): Generator { + 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('/') } diff --git a/packages/core/src/service/index.ts b/packages/core/src/service/index.ts index 05abf06ae..20bc83629 100644 --- a/packages/core/src/service/index.ts +++ b/packages/core/src/service/index.ts @@ -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' diff --git a/packages/core/src/source/LanguageError.ts b/packages/core/src/source/LanguageError.ts index 5b5178b31..ca645acc7 100644 --- a/packages/core/src/source/LanguageError.ts +++ b/packages/core/src/source/LanguageError.ts @@ -76,4 +76,7 @@ export type CodeActionChange = { type: 'edit' range: Range text: string +} | { + type: 'create' + uri: string } diff --git a/packages/java-edition/src/binder/index.ts b/packages/java-edition/src/binder/index.ts index f981b8913..a69088078 100644 --- a/packages/java-edition/src/binder/index.ts +++ b/packages/java-edition/src/binder/index.ts @@ -2,10 +2,12 @@ import type { CheckerContext, Config, FileCategory, + MetaRegistry, RootUriString, TaggableResourceLocationCategory, UriBinder, UriBinderContext, + UriBuilder, } from '@spyglassmc/core' import { ErrorSeverity, @@ -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[], @@ -179,6 +188,22 @@ export function* getRels( return undefined } +export function* getRoots( + uri: string, + rootUris: readonly RootUriString[], +): Generator { + 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 @@ -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() + 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)) + } +} diff --git a/packages/java-edition/src/index.ts b/packages/java-edition/src/index.ts index 8826c1c03..33b5c0657 100644 --- a/packages/java-edition/src/index.ts +++ b/packages/java-edition/src/index.ts @@ -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, @@ -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) { diff --git a/packages/language-server/src/util/toLS.ts b/packages/language-server/src/util/toLS.ts index 41efbcc07..7531adb20 100644 --- a/packages/language-server/src/util/toLS.ts +++ b/packages/language-server/src/util/toLS.ts @@ -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 } }), }