Skip to content

Commit

Permalink
🚧 Add code actions and commands
Browse files Browse the repository at this point in the history
  • Loading branch information
misode committed Dec 30, 2024
1 parent 076a83d commit 53af13d
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 8 deletions.
20 changes: 20 additions & 0 deletions packages/core/src/processor/codeActions/CodeAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { AstNode } from '../../node/index.js'
import type { CodeActionProviderContext } from '../../service/index.js'
import type { LanguageError } from '../../source/index.js'

export interface CodeAction {
title: string
command: CodeActionCommand
isPreferred?: boolean
errors?: LanguageError[]
}

export interface CodeActionCommand {
id: string
data?: unknown
}

export type CodeActionProvider<N extends AstNode = AstNode> = (
node: N,
ctx: CodeActionProviderContext,
) => readonly CodeAction[]
41 changes: 41 additions & 0 deletions packages/core/src/processor/codeActions/builtin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { AstNode } from '../../node/index.js'
import { FileNode } from '../../node/index.js'
import type { MetaRegistry } from '../../service/index.js'
import { Range } from '../../source/index.js'
import { traversePreOrder } from '../util.js'
import type { CodeAction, CodeActionProvider } from './CodeAction.js'

export const fallback: CodeActionProvider<AstNode> = (node, ctx) => {
const ans: CodeAction[] = []
traversePreOrder(
node,
(node) => Range.containsRange(node.range, ctx.range, true),
(node) => ctx.meta.hasCodeActionProvider(node.type),
(node) => ans.push(...ctx.meta.getCodeActionProvider(node.type)(node, ctx)),
)
return ans
}

export const file: CodeActionProvider<FileNode<AstNode>> = (node, ctx) => {
const ans: CodeAction[] = []
for (const error of FileNode.getErrors(node)) {
const action = error.info?.codeAction
if (!action) {
continue
}
if (!Range.containsRange(error.range, ctx.range, true)) {
continue
}
ans.push({
title: action.title,
isPreferred: action.isPreferred,
errors: [error],
command: action.command,
})
}
return ans
}

export function registerProviders(meta: MetaRegistry) {
meta.registerCodeActionProvider<FileNode<AstNode>>('file', file)
}
2 changes: 2 additions & 0 deletions packages/core/src/processor/codeActions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as codeActions from './builtin.js'
export * from './CodeAction.js'
1 change: 1 addition & 0 deletions packages/core/src/processor/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './binder/index.js'
export * from './checker/index.js'
export * from './codeActions/index.js'
export * from './ColorInfoProvider.js'
export * from './colorizer/index.js'
export * from './completer/index.js'
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/processor/linter/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isAllowedCharacter } from '../../parser/index.js'
import type { MetaRegistry, QuoteConfig } from '../../service/index.js'
import { SymbolLinterConfig } from '../../service/index.js'
import { McdocCategories } from '../../symbol/index.js'
import { undeclaredSymbol } from './builtin/undeclaredSymbol.js'
import { createUndeclaredFile, undeclaredSymbol } from './builtin/undeclaredSymbol.js'
import type { Linter } from './Linter.js'

export const noop: Linter<AstNode> = () => {}
Expand Down Expand Up @@ -130,3 +130,7 @@ export function registerLinters(meta: MetaRegistry) {
nodePredicate: (n) => n.symbol && !McdocCategories.includes(n.symbol.category as any),
})
}

export function registerCommands(meta: MetaRegistry) {
meta.registerCommand('create_undeclared_file', createUndeclaredFile)
}
27 changes: 24 additions & 3 deletions packages/core/src/processor/linter/builtin/undeclaredSymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { localeQuote, localize } from '@spyglassmc/locales'
import type { DeepReadonly } from '../../../common/index.js'
import { Arrayable, ResourceLocation } from '../../../common/index.js'
import type { AstNode } from '../../../node/index.js'
import type { Command } from '../../../service/Command.js'
import type { LinterContext } from '../../../service/index.js'
import { LinterSeverity, SymbolLinterConfig as Config } from '../../../service/index.js'
import type { Symbol } from '../../../symbol/index.js'
import { SymbolUtil, SymbolVisibility } from '../../../symbol/index.js'
import type { LanguageErrorInfo } from '../../../source/LanguageError.js'
import type { FileCategory, Symbol } from '../../../symbol/index.js'
import { FileCategories, SymbolUtil, SymbolVisibility } from '../../../symbol/index.js'
import type { Linter } from '../Linter.js'

export const undeclaredSymbol: Linter<AstNode> = (node, ctx) => {
Expand All @@ -20,6 +22,21 @@ export const undeclaredSymbol: Linter<AstNode> = (node, ctx) => {
})
}
if (Config.Action.isReport(action)) {
const info = FileCategories.includes(node.symbol.category as FileCategory)
? {
codeAction: {
title: localize(
'code-action.create-undeclared-file',
node.symbol.category,
localeQuote(node.symbol.identifier),
),
command: {
id: 'create_undeclared_file',
data: { category: node.symbol.category, identifier: node.symbol.identifier },
},
},
} satisfies LanguageErrorInfo
: undefined
const severityOverride = action.report === 'inherit'
? undefined
: LinterSeverity.toErrorSeverity(action.report)
Expand All @@ -30,7 +47,7 @@ export const undeclaredSymbol: Linter<AstNode> = (node, ctx) => {
localeQuote(node.symbol.identifier),
),
node,
undefined,
info,
severityOverride,
)
}
Expand Down Expand Up @@ -112,3 +129,7 @@ function getVisibility(
return SymbolVisibility.Public
}
}

export const createUndeclaredFile: Command = (data, ctx) => {
// TODO
}
3 changes: 3 additions & 0 deletions packages/core/src/service/Command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { CommandContext } from '../service/index.js'

export type Command = (options: unknown, ctx: CommandContext) => void
34 changes: 34 additions & 0 deletions packages/core/src/service/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,21 @@ export namespace FormatterContext {
}
}

export interface CodeActionProviderContext extends ProcessorContext {
range: Range
}
export interface CodeActionProviderContextOptions extends ProcessorContextOptions {
range: Range
}
export namespace CodeActionProviderContext {
export function create(
project: ProjectData,
opts: CodeActionProviderContextOptions,
): CodeActionProviderContext {
return { ...ProcessorContext.create(project, opts), range: opts.range }
}
}

export interface ColorizerContext extends ProcessorWithRangeContext {}
export interface ColorizerContextOptions extends ProcessorWithRangeContextOptions {}
export namespace ColorizerContext {
Expand Down Expand Up @@ -225,6 +240,25 @@ export namespace SignatureHelpProviderContext {
}
}

export interface CommandContext extends ContextBase {
config: Config
symbols: SymbolUtil
uri?: string
}
export interface CommandContextOptions {
uri?: string
}
export namespace CommandContext {
export function create(project: ProjectData, opts: CommandContextOptions): CommandContext {
return {
...ContextBase.create(project),
config: project.config,
symbols: project.symbols,
uri: opts.uri,
}
}
}

export interface UriBinderContext extends ContextBase {
config: Config
symbols: SymbolUtil
Expand Down
39 changes: 38 additions & 1 deletion packages/core/src/service/MetaRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ import type { Formatter } from '../processor/formatter/index.js'
import type {
Binder,
Checker,
CodeActionProvider,
Colorizer,
Completer,
InlayHintProvider,
} from '../processor/index.js'
import { binder, checker, colorizer, completer, formatter, linter } from '../processor/index.js'
import {
binder,
checker,
codeActions,
colorizer,
completer,
formatter,
linter,
} from '../processor/index.js'
import type { Linter } from '../processor/linter/Linter.js'
import type { SignatureHelpProvider } from '../processor/SignatureHelpProvider.js'
import type { Command } from './Command.js'
import type { DependencyKey, DependencyProvider } from './Dependency.js'
import type { FileExtension } from './fileUtil.js'
import type { SymbolRegistrar } from './SymbolRegistrar.js'
Expand Down Expand Up @@ -56,6 +66,8 @@ export class MetaRegistry {
readonly #binders = new Map<string, Binder<any>>()
readonly #checkers = new Map<string, Checker<any>>()
readonly #colorizers = new Map<string, Colorizer<any>>()
readonly #codeActionProviders = new Map<string, CodeActionProvider<any>>()
readonly #commands = new Map<string, Command>()
readonly #completers = new Map<string, Completer<any>>()
readonly #dependencyProviders = new Map<DependencyKey, DependencyProvider>()
readonly #formatters = new Map<string, Formatter<any>>()
Expand All @@ -71,10 +83,12 @@ export class MetaRegistry {
constructor() {
binder.registerBinders(this)
checker.registerCheckers(this)
codeActions.registerProviders(this)
colorizer.registerColorizers(this)
completer.registerCompleters(this)
formatter.registerFormatters(this)
linter.registerLinters(this)
linter.registerCommands(this)
}

/**
Expand Down Expand Up @@ -144,6 +158,29 @@ export class MetaRegistry {
this.#checkers.set(type, checker)
}

public hasCodeActionProvider<N extends AstNode>(type: N['type']): boolean {
return this.#codeActionProviders.has(type)
}
public getCodeActionProvider<N extends AstNode>(type: N['type']): CodeActionProvider<N> {
return this.#codeActionProviders.get(type) ?? codeActions.fallback
}
public registerCodeActionProvider<N extends AstNode>(
type: N['type'],
codeActionProvider: CodeActionProvider<N>,
): void {
this.#codeActionProviders.set(type, codeActionProvider)
}

public getCommandIds(): string[] {
return [...this.#commands.keys()]
}
public getCommand(id: string): Command | undefined {
return this.#commands.get(id)
}
public registerCommand(id: string, command: Command): void {
this.#commands.set(id, command)
}

public hasColorizer<N extends AstNode>(type: N['type']): boolean {
return this.#colorizers.has(type)
}
Expand Down
43 changes: 42 additions & 1 deletion packages/core/src/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ import type { TextDocument } from 'vscode-languageserver-textdocument'
import type { Logger } from '../common/index.js'
import type { FileNode } from '../node/index.js'
import { AstNode } from '../node/index.js'
import type { Color, ColorInfo, ColorToken, InlayHint, SignatureHelp } from '../processor/index.js'
import type {
CodeAction,
Color,
ColorInfo,
ColorToken,
InlayHint,
SignatureHelp,
} from '../processor/index.js'
import { ColorPresentation, completer, traversePreOrder } from '../processor/index.js'
import { Range } from '../source/index.js'
import type { SymbolLocation, SymbolUsageType } from '../symbol/index.js'
import { SymbolUsageTypes } from '../symbol/index.js'
import {
CodeActionProviderContext,
ColorizerContext,
CommandContext,
CompleterContext,
ContextBase,
FormatterContext,
ProcessorContext,
SignatureHelpProviderContext,
Expand Down Expand Up @@ -61,6 +71,18 @@ export class Service {
return []
}

getCodeActions(node: FileNode<AstNode>, doc: TextDocument, range: Range): readonly CodeAction[] {
try {
this.debug(`Getting code actions ${doc.uri} # ${doc.version} @ ${Range.toString(range)}`)
const codeActionProvider = this.project.meta.getCodeActionProvider(node.type)
const ctx = CodeActionProviderContext.create(this.project, { doc, range })
return codeActionProvider(node, ctx)
} catch (e) {
this.logger.error(`[Service] [getCodeActions] Failed for ${doc.uri} # ${doc.version}`, e)
}
return []
}

getColorInfo(node: FileNode<AstNode>, doc: TextDocument): ColorInfo[] {
try {
this.debug(`Getting color info for ${doc.uri} # ${doc.version}`)
Expand Down Expand Up @@ -147,6 +169,25 @@ export class Service {
)
}

executeCommand(id: string, args: unknown[]) {
try {
this.debug(`Executing command ${id} with: ${args}`)
const command = this.project.meta.getCommand(id)
if (command) {
const uri = args.length >= 1 && typeof args[0] === 'string' ? args[0] : undefined
const data = args.length >= 2 ? args[1] : {}
command(data, CommandContext.create(this.project, { uri }))
} else {
this.logger.warn(
`[Service] [executeCommand] Tried to execute nonexistant command ${id}`,
)
}
} catch (e) {
this.logger.error(`[Service] [executeCommand] Failed to execute ${id}`, e)
throw e
}
}

format(node: FileNode<AstNode>, doc: TextDocument, tabSize: number, insertSpaces: boolean) {
try {
this.debug(`Formatting ${doc.uri} # ${doc.version}`)
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/source/LanguageError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TextDocument } from 'vscode-languageserver-textdocument'
import type { CodeActionCommand } from '../index.js'
import type { Location } from './Location.js'
import { PositionRange } from './PositionRange.js'
import type { Range } from './Range.js'
Expand Down Expand Up @@ -60,8 +61,14 @@ export const enum ErrorSeverity {
}

export interface LanguageErrorInfo {
codeAction?: string
codeAction?: LanguageErrorAction
deprecated?: boolean
unnecessary?: boolean
related?: { location: Location; message: string }[]
}

export interface LanguageErrorAction {
title: string
command: CodeActionCommand
isPreferred?: boolean
}
Loading

0 comments on commit 53af13d

Please sign in to comment.