Skip to content

Commit d86aa2f

Browse files
authored
Code actions (#156)
1 parent 9eac6c0 commit d86aa2f

File tree

11 files changed

+319
-11
lines changed

11 files changed

+319
-11
lines changed

packages/analyzer/src/issue.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Position } from "@knuckles/location";
1+
import type { Position, Range } from "@knuckles/location";
22

33
export enum AnalyzerSeverity {
44
Error = "error",
@@ -11,4 +11,15 @@ export interface AnalyzerIssue {
1111
message: string;
1212
start: Position | undefined;
1313
end: Position | undefined;
14+
quickFix?: AnalyzerQuickFix | undefined;
15+
}
16+
17+
export interface AnalyzerQuickFix {
18+
label?: string | undefined;
19+
edits: AnalyzerQuickFixEdit[];
20+
}
21+
22+
export interface AnalyzerQuickFixEdit {
23+
range: Range;
24+
text: string;
1425
}

packages/analyzer/src/standard/rules/virtual-element-end-notation.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ export default {
1010
check({ report, document }) {
1111
document.visit(
1212
(node): void => {
13-
const regex = new RegExp(
14-
`\\/ko\\s+${escapeStringRegexp(node.binding.name.value)}`,
15-
);
13+
const bindingName = node.binding.name.value;
14+
const regex = new RegExp(`\\/ko\\s+${escapeStringRegexp(bindingName)}`);
1615

1716
if (!regex.test(node.endComment.content)) {
1817
report({
@@ -21,6 +20,15 @@ export default {
2120
severity: this.severity,
2221
start: node.endComment.start,
2322
end: node.endComment.end,
23+
quickFix: {
24+
label: "Add notation",
25+
edits: [
26+
{
27+
range: node.endComment,
28+
text: `<!-- /ko ${bindingName} -->`,
29+
},
30+
],
31+
},
2432
});
2533
}
2634
},
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { LanguageServiceWorker } from "../private.js";
2+
import {
3+
type ProtocolPosition,
4+
type ProtocolRange,
5+
} from "../utils/position.js";
6+
import { Range } from "@knuckles/location";
7+
8+
export interface DiagnosticIdentifier {
9+
code: string;
10+
range: ProtocolRange;
11+
}
12+
13+
export interface CodeActionParams {
14+
fileName: string;
15+
position: ProtocolPosition;
16+
diagnostics?: DiagnosticIdentifier[];
17+
}
18+
19+
export interface CodeAction {
20+
label: string;
21+
edits: CodeActionEdit[];
22+
diagnostic?: DiagnosticIdentifier;
23+
}
24+
25+
export type CodeActionEdit =
26+
| {
27+
type: "create-file";
28+
fileName: string;
29+
}
30+
| {
31+
type: "delete-file";
32+
fileName: string;
33+
}
34+
| {
35+
type: "rename-file";
36+
oldFileName: string;
37+
newFileName: string;
38+
}
39+
| {
40+
type: "delete";
41+
fileName: string;
42+
range: ProtocolRange;
43+
}
44+
| {
45+
type: "replace";
46+
fileName: string;
47+
range: ProtocolRange;
48+
text: string;
49+
}
50+
| {
51+
type: "insert";
52+
fileName: string;
53+
position: ProtocolPosition;
54+
text: string;
55+
};
56+
57+
export type CodeActions = CodeAction[];
58+
59+
export default async function getCodeActions(
60+
this: LanguageServiceWorker,
61+
params: CodeActionParams,
62+
): Promise<CodeActions> {
63+
const state = await this.getDocumentState(params.fileName);
64+
if (state.broken) return [];
65+
66+
const codeActions: CodeActions = [];
67+
68+
for (const issue of state.issues) {
69+
if (issue.quickFix) {
70+
const diagnostic = (params.diagnostics ?? []).find((diagnostic) =>
71+
Range.fromLinesAndColumns(
72+
diagnostic.range.start.line,
73+
diagnostic.range.start.column,
74+
diagnostic.range.end.line,
75+
diagnostic.range.end.column,
76+
state.document.text,
77+
),
78+
);
79+
const label = issue.quickFix.label ?? "Fix this issue";
80+
const edits = issue.quickFix.edits.map((edit): CodeActionEdit => {
81+
if (edit.text.length === 0) {
82+
return {
83+
type: "delete",
84+
fileName: params.fileName,
85+
range: edit.range,
86+
};
87+
} else if (edit.range.size === 0) {
88+
return {
89+
type: "insert",
90+
fileName: params.fileName,
91+
position: edit.range.start.toJSON(),
92+
text: edit.text,
93+
};
94+
} else {
95+
return {
96+
type: "replace",
97+
fileName: params.fileName,
98+
range: edit.range.toJSON(),
99+
text: edit.text,
100+
};
101+
}
102+
});
103+
codeActions.push({
104+
label,
105+
edits,
106+
diagnostic,
107+
});
108+
}
109+
}
110+
111+
return codeActions;
112+
}

packages/language-service/src/features/completion.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export default async function getCompletion(
8686
includeCompletionsForImportStatements: false,
8787
includeCompletionsForModuleExports: false,
8888
allowRenameOfImportPath: false,
89-
// TODO: get quote from current binding attribute
9089
quotePreference,
9190
triggerCharacter: params.context?.triggerCharacter as
9291
| ts.CompletionsTriggerCharacter

packages/language-service/src/features/diagnostics.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { LanguageServiceWorker } from "../private.js";
2+
import type { Document } from "../utils/document.js";
3+
import { getFullIssueRange } from "../utils/issue.js";
24
import type { ProtocolRange } from "../utils/position.js";
35
import { AnalyzerSeverity, type AnalyzerIssue } from "@knuckles/analyzer";
4-
import { Position, Range } from "@knuckles/location";
56

67
export interface DiagnosticsParams {
78
fileName: string;
@@ -31,19 +32,17 @@ export default async function getDiagnostics(
3132
const state = await this.getDocumentState(params.fileName);
3233

3334
const diagnostics = state.issues.map((issue) =>
34-
translateIssueToDiagnostic(issue, state.document.text),
35+
translateIssueToDiagnostic(state.document, issue),
3536
);
3637

3738
return diagnostics;
3839
}
3940

4041
function translateIssueToDiagnostic(
42+
document: Document,
4143
issue: AnalyzerIssue,
42-
text: string,
4344
): Diagnostic {
44-
const start = issue.start ?? Position.fromOffset(0, text);
45-
const end = issue.end ?? Position.fromOffset(start.offset + 1, text);
46-
const range = new Range(start, end);
45+
const range = getFullIssueRange(document, issue);
4746
const severity = {
4847
[AnalyzerSeverity.Error]: DiagnosticSeverity.Error,
4948
[AnalyzerSeverity.Warning]: DiagnosticSeverity.Warning,

packages/language-service/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./public.js";
2+
export type * from "./features/code-actions.js";
23
export type * from "./features/completion.js";
34
export type * from "./features/definition.js";
45
export type * from "./features/diagnostics.js";

packages/language-service/src/private.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import getCodeActions from "./features/code-actions.js";
12
import getCompletion from "./features/completion.js";
23
import getDefinition from "./features/definition.js";
34
import getDiagnostics from "./features/diagnostics.js";
@@ -31,6 +32,7 @@ export class LanguageServiceWorker {
3132
"document/definition": getDefinition.bind(this),
3233
"document/diagnostics": getDiagnostics.bind(this),
3334
"document/hover": getHover.bind(this),
35+
"document/quick-fixes": getCodeActions.bind(this),
3436
};
3537

3638
#programProvider = new ProgramProvider();

packages/language-service/src/public.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { CodeActions, CodeActionParams } from "./features/code-actions.js";
12
import type { Completion, CompletionParams } from "./features/completion.js";
23
import type { Definition, DefinitionParams } from "./features/definition.js";
34
import type { Diagnostics, DiagnosticsParams } from "./features/diagnostics.js";
@@ -129,5 +130,9 @@ export class LanguageService {
129130
getHover(params: HoverParams): Promise<Hover | null> {
130131
return this.#client.request("document/hover", params);
131132
}
133+
134+
getCodeActions(params: CodeActionParams): Promise<CodeActions> {
135+
return this.#client.request("document/quick-fixes", params);
136+
}
132137
//#endregion
133138
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Document } from "./document.js";
2+
import type { AnalyzerIssue } from "@knuckles/analyzer";
3+
import { Position, Range } from "@knuckles/location";
4+
5+
export function getFullIssueRange(document: Document, issue: AnalyzerIssue) {
6+
const start = issue.start ?? Position.fromOffset(0, document.text);
7+
const end = issue.end ?? Position.fromOffset(start.offset + 1, document.text);
8+
const range = new Range(start, end);
9+
return range;
10+
}

0 commit comments

Comments
 (0)