Skip to content

Commit c836d7e

Browse files
authored
Merge pull request #133 from arpitkuriyal/codeAction
Added codeAction (extract subSchema to defs)
2 parents eb1508b + 050b459 commit c836d7e

27 files changed

+761
-84
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ dist/
55
.vscode/
66
scratch/
77
TODO
8+
.DS_Store
9+

language-server/package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

language-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"keywords": [],
1717
"dependencies": {
1818
"chokidar": "^4.0.1",
19+
"detect-indent": "^7.0.1",
1920
"ignore": "^7.0.1",
2021
"jsonc-parser": "^3.3.1",
2122
"merge-anything": "^6.0.2",

language-server/src/build-server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { ValidationErrorsDiagnosticsProvider } from "./features/diagnostics/vali
2222
import { IfThenCompletionProvider } from "./features/completion/if-then-completion.js";
2323
import { KeywordCompletionProvider } from "./features/completion/keyword-completion.js";
2424
import { SchemaCompletionProvider } from "./features/completion/schema-completion.js";
25+
import { ExtractSubSchemaToDefs } from "./features/codeAction/extractSubschema.js";
2526

2627
// Hyperjump
2728
import { removeMediaTypePlugin } from "@hyperjump/browser";
@@ -49,6 +50,7 @@ export const buildServer = (connection) => {
4950
new GotoDefinitionFeature(server, schemas);
5051
new FindReferencesFeature(server, schemas);
5152
new HoverFeature(server, schemas);
53+
new ExtractSubSchemaToDefs(server, schemas, configuration);
5254

5355
// TODO: It's awkward that diagnostics needs a variable
5456
const diagnostics = new DiagnosticsFeature(server, [
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import {
2+
CodeActionKind,
3+
TextDocumentEdit
4+
} from "vscode-languageserver";
5+
import { getKeywordName } from "@hyperjump/json-schema/experimental";
6+
import * as SchemaDocument from "../../model/schema-document.js";
7+
import * as SchemaNode from "../../model/schema-node.js";
8+
import { withFormatting } from "../../util/util.js";
9+
10+
/**
11+
* @import { Server } from "../../services/server.js";
12+
* @import { Schemas } from "../../services/schemas.js";
13+
* @import { CodeAction } from "vscode-languageserver";
14+
* @import { Configuration } from "../../services/configuration.js";
15+
*/
16+
17+
18+
export class ExtractSubSchemaToDefs {
19+
/**
20+
* @param {Server} server
21+
* @param {Schemas} schemas
22+
* @param {Configuration} configuration
23+
*/
24+
constructor(server, schemas, configuration) {
25+
this.server = server;
26+
this.schemas = schemas;
27+
this.configuration = configuration;
28+
29+
server.onInitialize(() => ({
30+
capabilities: {
31+
codeActionProvider: true
32+
}
33+
}));
34+
35+
server.onCodeAction(async ({ textDocument, range }) => {
36+
if (range.start.line === range.end.line && range.start.character === range.end.character) {
37+
return [];
38+
}
39+
40+
const uri = textDocument.uri;
41+
let schemaDocument = await schemas.getOpen(uri);
42+
if (!schemaDocument) {
43+
return [];
44+
}
45+
46+
const offset = schemaDocument.textDocument.offsetAt(range.start);
47+
const node = SchemaDocument.findNodeAtOffset(schemaDocument, offset);
48+
if (!node?.isSchema) {
49+
return [];
50+
}
51+
52+
const dialectUri = /** @type {string} */ (node.root.dialectUri);
53+
const definitionsKeyword = getKeywordName(dialectUri, "https://json-schema.org/keyword/definitions");
54+
const definitionsNode = SchemaNode.step(definitionsKeyword, node.root);
55+
let highestDefNumber = 0;
56+
if (definitionsNode) {
57+
let defNodeKeys = SchemaNode.keys(definitionsNode);
58+
for (const key of defNodeKeys) {
59+
const keyValue = /** @type {string} */ (SchemaNode.value(key));
60+
61+
const match = /^def(\d+)$/.exec(keyValue);
62+
if (match) {
63+
highestDefNumber = Math.max(parseInt(match[1], 10), highestDefNumber);
64+
}
65+
}
66+
}
67+
68+
const newDefName = `def${highestDefNumber + 1}`;
69+
const extractedDef = schemaDocument.textDocument.getText(range);
70+
const settings = await this.configuration.get();
71+
const lastSubschema = node.root.children.slice(-1)[0];
72+
const lastSubschemaPosition = lastSubschema.offset + lastSubschema.textLength;
73+
const lastDefinition = definitionsNode?.children.at(-1);
74+
const lastDefinitionPosition = (lastDefinition)
75+
? lastDefinition.offset + lastDefinition.textLength
76+
: /** @type {number} */ (definitionsNode?.offset) + 1;
77+
/** @type {CodeAction} */
78+
const codeAction = {
79+
title: `Extract '${newDefName}' to ${definitionsKeyword}`,
80+
kind: CodeActionKind.RefactorExtract,
81+
edit: {
82+
documentChanges: [
83+
TextDocumentEdit.create({ uri: textDocument.uri, version: null }, [
84+
{
85+
range: range,
86+
newText: `{ "$ref": "#/${definitionsKeyword}/${newDefName}" }`
87+
},
88+
definitionsNode
89+
? withFormatting(schemaDocument.textDocument, {
90+
range: {
91+
start: schemaDocument.textDocument.positionAt(lastDefinitionPosition),
92+
end: schemaDocument.textDocument.positionAt(lastDefinitionPosition)
93+
},
94+
newText: lastDefinition ? `,\n"${newDefName}": ${extractedDef}` : `\n"${newDefName}": ${extractedDef}`
95+
}, settings)
96+
: withFormatting(schemaDocument.textDocument, {
97+
range: {
98+
start: schemaDocument.textDocument.positionAt(lastSubschemaPosition),
99+
end: schemaDocument.textDocument.positionAt(lastSubschemaPosition)
100+
},
101+
newText: `,\n"${definitionsKeyword}": {\n"${newDefName}": ${extractedDef}\n}`
102+
}, settings)
103+
])
104+
]
105+
}
106+
};
107+
108+
return [codeAction];
109+
});
110+
}
111+
}

0 commit comments

Comments
 (0)