-
-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor to give things more structure
- Loading branch information
1 parent
6a12c9d
commit 4fd9a2a
Showing
13 changed files
with
1,236 additions
and
602 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { getDialectIds } from "@hyperjump/json-schema/experimental"; | ||
import { getSchemaDocument } from "./schema-documents.js"; | ||
import { CompletionItemKind } from "vscode-languageserver"; | ||
|
||
|
||
export default { | ||
onInitialize() { | ||
return { | ||
completionProvider: { | ||
resolveProvider: false, | ||
triggerCharacters: ["\"", ":", " "] | ||
} | ||
}; | ||
}, | ||
|
||
onInitialized(connection, documents) { | ||
connection.onCompletion(async ({ textDocument, position }) => { | ||
const document = documents.get(textDocument.uri); | ||
const schemaDocument = await getSchemaDocument(connection, document); | ||
const offset = document.offsetAt(position); | ||
const currentProperty = schemaDocument.findNodeAtOffset(offset); | ||
if (currentProperty.pointer.endsWith("/$schema")) { | ||
return getDialectIds().map((uri) => { | ||
return { | ||
label: shouldHaveTrailingHash(uri) ? `${uri}#` : uri, | ||
kind: CompletionItemKind.Value | ||
}; | ||
}); | ||
} | ||
}); | ||
|
||
const trailingHashDialects = new Set([ | ||
"http://json-schema.org/draft-04/schema", | ||
"http://json-schema.org/draft-06/schema", | ||
"http://json-schema.org/draft-07/schema" | ||
]); | ||
const shouldHaveTrailingHash = (uri) => trailingHashDialects.has(uri); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { DiagnosticSeverity, DiagnosticTag } from "vscode-languageserver"; | ||
import * as Instance from "../json-instance.js"; | ||
import { subscribe } from "../pubsub.js"; | ||
|
||
|
||
export default { | ||
onInitialize() { | ||
return {}; | ||
}, | ||
|
||
onInitialized() { | ||
subscribe("diagnostics", async (_message, { schemaDocument, diagnostics }) => { | ||
for (const deprecated of schemaDocument.annotatedWith("deprecated")) { | ||
if (Instance.annotation(deprecated, "deprecated").some((deprecated) => deprecated)) { | ||
diagnostics.push({ | ||
instance: deprecated.parent, | ||
message: Instance.annotation(deprecated, "x-deprecationMessage").join("\n") || "deprecated", | ||
severity: DiagnosticSeverity.Warning, | ||
tags: [DiagnosticTag.Deprecated] | ||
}); | ||
} | ||
} | ||
}); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { DidChangeConfigurationNotification } from "vscode-languageserver"; | ||
import { publish } from "../pubsub.js"; | ||
|
||
|
||
export const isSchema = RegExp.prototype.test.bind(/(?:\.|\/|^)schema\.json$/); | ||
|
||
let hasConfigurationCapability = false; | ||
let hasDidChangeConfigurationCapability = false; | ||
|
||
export default { | ||
onInitialize: ({ capabilities }) => { | ||
hasConfigurationCapability = !!capabilities.workspace?.configuration; | ||
hasDidChangeConfigurationCapability = !!capabilities.workspace?.didChangeConfiguration?.dynamicRegistration; | ||
|
||
return {}; | ||
}, | ||
|
||
onInitialized: (connection, documents) => { | ||
if (hasDidChangeConfigurationCapability) { | ||
connection.client.register(DidChangeConfigurationNotification.type); | ||
} | ||
|
||
connection.onDidChangeConfiguration((change) => { | ||
if (hasConfigurationCapability) { | ||
documentSettings.clear(); | ||
} else { | ||
globalSettings = change.settings.jsonSchemaLanguageServer ?? globalSettings; | ||
} | ||
|
||
publish("workspaceChange", { changes: [] }); | ||
}); | ||
|
||
documents.onDidClose(({ document }) => { | ||
documentSettings.delete(document.uri); | ||
}); | ||
} | ||
}; | ||
|
||
const documentSettings = new Map(); | ||
let globalSettings = {}; | ||
|
||
export const getDocumentSettings = async (connection, uri) => { | ||
if (!hasConfigurationCapability) { | ||
return globalSettings; | ||
} | ||
|
||
if (!documentSettings.has(uri)) { | ||
const result = await connection.workspace.getConfiguration({ | ||
scopeUri: uri, | ||
section: "jsonSchemaLanguageServer" | ||
}); | ||
documentSettings.set(uri, result ?? globalSettings); | ||
} | ||
|
||
return documentSettings.get(uri); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { MarkupKind } from "vscode-languageserver"; | ||
import * as Instance from "../json-instance.js"; | ||
import { getSchemaDocument } from "./schema-documents.js"; | ||
|
||
|
||
export default { | ||
onInitialize() { | ||
return { | ||
hoverProvider: true | ||
}; | ||
}, | ||
|
||
onInitialized(connection, documents) { | ||
connection.onHover(async ({ textDocument, position }) => { | ||
const document = documents.get(textDocument.uri); | ||
const schemaDocument = await getSchemaDocument(connection, document); | ||
const offset = document.offsetAt(position); | ||
const keyword = schemaDocument.findNodeAtOffset(offset); | ||
|
||
// This is a little wierd because we want the hover to be on the keyword, but | ||
// the annotation is actually on the value not the keyword. | ||
if (keyword.parent && Instance.typeOf(keyword.parent) === "property" && keyword.parent.children[0] === keyword) { | ||
return { | ||
contents: { | ||
kind: MarkupKind.Markdown, | ||
value: Instance.annotation(keyword.parent.children[1], "description").join("\n") | ||
}, | ||
range: { | ||
start: document.positionAt(keyword.offset), | ||
end: document.positionAt(keyword.offset + keyword.textLength) | ||
} | ||
}; | ||
} | ||
}); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { getDocumentSettings } from "./document-settings.js"; | ||
import { JsonSchemaDocument } from "../json-schema-document.js"; | ||
|
||
|
||
export default { | ||
onInitialize() { | ||
return {}; | ||
}, | ||
|
||
onInitialized(_connection, documents) { | ||
documents.onDidClose(({ document }) => { | ||
schemaDocuments.delete(document.uri); | ||
}); | ||
} | ||
}; | ||
|
||
const schemaDocuments = new Map(); | ||
|
||
export const getSchemaDocument = async (connection, textDocument) => { | ||
let { version, schemaDocument } = schemaDocuments.get(textDocument.uri) ?? {}; | ||
|
||
if (version !== textDocument.version) { | ||
const settings = await getDocumentSettings(connection, textDocument.uri); | ||
schemaDocument = await JsonSchemaDocument.fromTextDocument(textDocument, settings.defaultDialect); | ||
|
||
if (textDocument.version !== -1) { | ||
schemaDocuments.set(textDocument.uri, { version: textDocument.version, schemaDocument }); | ||
} | ||
} | ||
|
||
return schemaDocument; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
import { SemanticTokensBuilder } from "vscode-languageserver"; | ||
import { getKeywordId } from "@hyperjump/json-schema/experimental"; | ||
import * as Instance from "../json-instance.js"; | ||
import { getSchemaDocument } from "./schema-documents.js"; | ||
import { toAbsoluteUri } from "../util.js"; | ||
import { isSchema } from "./document-settings.js"; | ||
|
||
|
||
export default { | ||
onInitialize({ capabilities }) { | ||
return { | ||
semanticTokensProvider: { | ||
legend: buildSemanticTokensLegend(capabilities.textDocument?.semanticTokens), | ||
range: false, | ||
full: { | ||
delta: true | ||
} | ||
} | ||
}; | ||
}, | ||
|
||
onInitialized(connection, documents) { | ||
const tokenBuilders = new Map(); | ||
|
||
documents.onDidClose(({ document }) => { | ||
tokenBuilders.delete(document.uri); | ||
}); | ||
|
||
const getTokenBuilder = (uri) => { | ||
if (!tokenBuilders.has(uri)) { | ||
tokenBuilders.set(uri, new SemanticTokensBuilder()); | ||
} | ||
|
||
return tokenBuilders.get(uri); | ||
}; | ||
|
||
const buildTokens = async (builder, uri) => { | ||
const textDocument = documents.get(uri); | ||
const schemaDocument = await getSchemaDocument(connection, textDocument); | ||
const semanticTokens = getSemanticTokens(schemaDocument); | ||
for (const { keywordInstance, tokenType, tokenModifier } of sortSemanticTokens(semanticTokens, textDocument)) { | ||
const startPosition = textDocument.positionAt(keywordInstance.offset); | ||
builder.push( | ||
startPosition.line, | ||
startPosition.character, | ||
keywordInstance.textLength, | ||
semanticTokensLegend.tokenTypes[tokenType] ?? 0, | ||
semanticTokensLegend.tokenModifiers[tokenModifier] ?? 0 | ||
); | ||
} | ||
}; | ||
|
||
connection.languages.semanticTokens.on(async ({ textDocument }) => { | ||
if (!isSchema(textDocument.uri)) { | ||
return { data: [] }; | ||
} | ||
|
||
const builder = getTokenBuilder(textDocument.uri); | ||
await buildTokens(builder, textDocument.uri); | ||
|
||
return builder.build(); | ||
}); | ||
|
||
connection.languages.semanticTokens.onDelta(async ({ textDocument, previousResultId }) => { | ||
const builder = getTokenBuilder(textDocument.uri); | ||
builder.previousResult(previousResultId); | ||
await buildTokens(builder, textDocument.uri); | ||
|
||
return builder.buildEdits(); | ||
}); | ||
} | ||
}; | ||
|
||
const semanticTokensLegend = { | ||
tokenTypes: {}, | ||
tokenModifiers: {} | ||
}; | ||
|
||
const buildSemanticTokensLegend = (capability) => { | ||
const clientTokenTypes = new Set(capability.tokenTypes); | ||
const serverTokenTypes = [ | ||
"property", | ||
"keyword", | ||
"comment", | ||
"string", | ||
"regexp" | ||
]; | ||
|
||
const tokenTypes = []; | ||
for (const tokenType of serverTokenTypes) { | ||
if (clientTokenTypes.has(tokenType)) { | ||
semanticTokensLegend.tokenTypes[tokenType] = tokenTypes.length; | ||
tokenTypes.push(tokenType); | ||
} | ||
} | ||
|
||
const clientTokenModifiers = new Set(capability.tokenModifiers); | ||
const serverTokenModifiers = [ | ||
]; | ||
|
||
const tokenModifiers = []; | ||
for (const tokenModifier of serverTokenModifiers) { | ||
if (clientTokenModifiers.has(tokenModifier)) { | ||
semanticTokensLegend.tokenModifiers[tokenModifier] = tokenModifiers.length; | ||
tokenModifiers.push(tokenModifier); | ||
} | ||
} | ||
|
||
return { tokenTypes, tokenModifiers }; | ||
}; | ||
|
||
// VSCode requires this list to be in order. Neovim doesn't care. | ||
const sortSemanticTokens = (semanticTokens, textDocument) => { | ||
return [...semanticTokens].sort((a, b) => { | ||
const aStartPosition = textDocument.positionAt(a.keywordInstance.offset); | ||
const bStartPosition = textDocument.positionAt(b.keywordInstance.offset); | ||
|
||
return aStartPosition.line === bStartPosition.line | ||
? aStartPosition.character - bStartPosition.character | ||
: aStartPosition.line - bStartPosition.line; | ||
}); | ||
}; | ||
|
||
const getSemanticTokens = function* (schemaDocument) { | ||
for (const { schemaResource, dialectUri } of schemaDocument.schemaResources) { | ||
yield* schemaHandler(schemaResource, dialectUri); | ||
} | ||
}; | ||
|
||
const schemaHandler = function* (schemaResource, dialectUri) { | ||
for (const [keyNode, valueNode] of Instance.entries(schemaResource)) { | ||
const keywordName = Instance.value(keyNode); | ||
const keywordId = keywordIdFor(keywordName, dialectUri); | ||
|
||
if (keywordId) { | ||
if (keywordId === "https://json-schema.org/keyword/comment") { | ||
yield { keywordInstance: keyNode.parent, tokenType: "comment" }; | ||
} else if (toAbsoluteUri(keywordId) !== "https://json-schema.org/keyword/unknown") { | ||
yield { keywordInstance: keyNode, tokenType: "keyword" }; | ||
yield* getKeywordHandler(keywordId)(valueNode, dialectUri); | ||
} | ||
} | ||
} | ||
}; | ||
|
||
const keywordIdFor = (keywordName, dialectUri) => { | ||
try { | ||
return keywordName === "$schema" | ||
? "https://json-schema.org/keyword/schema" | ||
: getKeywordId(keywordName, dialectUri); | ||
} catch (error) { | ||
return; | ||
} | ||
}; | ||
|
||
const schemaMapHandler = function* (schemaResource, dialectUri) { | ||
for (const schemaNode of Instance.values(schemaResource)) { | ||
yield* schemaHandler(schemaNode, dialectUri); | ||
} | ||
}; | ||
|
||
const schemaArrayHandler = function* (schemaResource, dialectUri) { | ||
for (const schemaNode of Instance.iter(schemaResource)) { | ||
yield* schemaHandler(schemaNode, dialectUri); | ||
} | ||
}; | ||
|
||
const noopKeywordHandler = function* () {}; | ||
const getKeywordHandler = (keywordId) => keywordId in keywordHandlers ? keywordHandlers[keywordId] : noopKeywordHandler; | ||
|
||
const keywordHandlers = { | ||
"https://json-schema.org/keyword/additionalProperties": schemaHandler, | ||
"https://json-schema.org/keyword/allOf": schemaArrayHandler, | ||
"https://json-schema.org/keyword/anyOf": schemaArrayHandler, | ||
"https://json-schema.org/keyword/contains": schemaHandler, | ||
"https://json-schema.org/keyword/definitions": schemaMapHandler, | ||
"https://json-schema.org/keyword/dependentSchemas": schemaMapHandler, | ||
"https://json-schema.org/keyword/if": schemaHandler, | ||
"https://json-schema.org/keyword/then": schemaHandler, | ||
"https://json-schema.org/keyword/else": schemaHandler, | ||
"https://json-schema.org/keyword/items": schemaArrayHandler, | ||
"https://json-schema.org/keyword/not": schemaHandler, | ||
"https://json-schema.org/keyword/oneOf": schemaArrayHandler, | ||
"https://json-schema.org/keyword/patternProperties": schemaMapHandler, | ||
"https://json-schema.org/keyword/prefixItems": schemaArrayHandler, | ||
"https://json-schema.org/keyword/properties": schemaMapHandler, | ||
"https://json-schema.org/keyword/propertyNames": schemaHandler, | ||
"https://json-schema.org/keyword/unevaluatedItems": schemaHandler, | ||
"https://json-schema.org/keyword/unevaluatedProperties": schemaHandler | ||
}; |
Oops, something went wrong.