-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve the JSON completion & hover building blocks (#622)
The json-languageservice API was a bit awkward to use. Had a bunch of weird type problems that nobody should really have to care about. In this PR, I use an adapter to make folks write Completion & Hover providers in a manner similar to what we do with liquid files. It's not 100% the same, but it's similar enough and abstracts the weird quirks like "return undefined not Promise<undefined> when you can't hover something".
- Loading branch information
1 parent
611fc3b
commit 05b928e
Showing
15 changed files
with
385 additions
and
284 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,6 @@ | ||
--- | ||
'@shopify/theme-language-server-common': patch | ||
'@shopify/theme-check-common': patch | ||
--- | ||
|
||
[internal] Rejig how we do JSON completion & Hover |
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
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 |
---|---|---|
@@ -1,3 +1,3 @@ | ||
export function deepGet(obj: any, path: string[]): any { | ||
export function deepGet(obj: any, path: (string | number)[]): any { | ||
return path.reduce((acc, key) => acc?.[key], obj); | ||
} |
120 changes: 120 additions & 0 deletions
120
packages/theme-language-server-common/src/json/JSONContributions.ts
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,120 @@ | ||
import { parseJSON, SourceCodeType } from '@shopify/theme-check-common'; | ||
import { | ||
CompletionsCollector, | ||
JSONPath, | ||
JSONWorkerContribution, | ||
MarkedString, | ||
} from 'vscode-json-languageservice'; | ||
import { AugmentedSourceCode, DocumentManager } from '../documents'; | ||
import { GetTranslationsForURI } from '../translations'; | ||
import { JSONCompletionProvider } from './completions/JSONCompletionProvider'; | ||
import { SchemaTranslationsCompletionProvider } from './completions/providers/SchemaTranslationCompletionProvider'; | ||
import { JSONHoverProvider } from './hover/JSONHoverProvider'; | ||
import { SchemaTranslationHoverProvider } from './hover/providers/SchemaTranslationHoverProvider'; | ||
import { TranslationPathHoverProvider } from './hover/providers/TranslationPathHoverProvider'; | ||
import { RequestContext } from './RequestContext'; | ||
import { findSchemaNode } from './utils'; | ||
|
||
/** The getInfoContribution API will only fallback if we return undefined synchronously */ | ||
const SKIP_CONTRIBUTION = undefined as any; | ||
|
||
/** | ||
* I'm not a fan of how json-languageservice does its feature contributions. It's too different | ||
* from everything else we do in here. | ||
* | ||
* Instead, we'll have this little adapter that makes the completions and hover providers feel | ||
* a bit more familiar. | ||
*/ | ||
export class JSONContributions implements JSONWorkerContribution { | ||
private hoverProviders: JSONHoverProvider[]; | ||
private completionProviders: JSONCompletionProvider[]; | ||
|
||
constructor( | ||
private documentManager: DocumentManager, | ||
getDefaultSchemaTranslations: GetTranslationsForURI, | ||
) { | ||
this.hoverProviders = [ | ||
new TranslationPathHoverProvider(), | ||
new SchemaTranslationHoverProvider(getDefaultSchemaTranslations), | ||
]; | ||
this.completionProviders = [ | ||
new SchemaTranslationsCompletionProvider(getDefaultSchemaTranslations), | ||
]; | ||
} | ||
|
||
getInfoContribution(uri: string, location: JSONPath): Promise<MarkedString[]> { | ||
const doc = this.documentManager.get(uri); | ||
if (!doc) return SKIP_CONTRIBUTION; | ||
const context = this.getContext(doc); | ||
const provider = this.hoverProviders.find((p) => p.canHover(context, location)); | ||
if (!provider) return SKIP_CONTRIBUTION; | ||
return provider.hover(context, location); | ||
} | ||
|
||
async collectPropertyCompletions( | ||
uri: string, | ||
location: JSONPath, | ||
// Don't know what those three are for. | ||
_currentWord: string, | ||
_addValue: boolean, | ||
_isLast: boolean, | ||
result: CompletionsCollector, | ||
): Promise<void> { | ||
const doc = this.documentManager.get(uri); | ||
if (!doc || doc.ast instanceof Error) return; | ||
|
||
const items = await Promise.all( | ||
this.completionProviders | ||
.filter((provider) => provider.completeProperty) | ||
.map((provider) => provider.completeProperty!(this.getContext(doc), location)), | ||
); | ||
|
||
for (const item of items.flat()) { | ||
result.add(item); | ||
} | ||
} | ||
|
||
async collectValueCompletions( | ||
uri: string, | ||
location: JSONPath, | ||
propertyKey: string, | ||
result: CompletionsCollector, | ||
): Promise<void> { | ||
const doc = this.documentManager.get(uri); | ||
if (!doc || doc.ast instanceof Error) return; | ||
|
||
const items = await Promise.all( | ||
this.completionProviders | ||
.filter((provider) => provider.completeValue) | ||
.map((provider) => | ||
provider.completeValue!(this.getContext(doc), location.concat(propertyKey)), | ||
), | ||
); | ||
|
||
for (const item of items.flat()) { | ||
result.add(item); | ||
} | ||
} | ||
|
||
/** I'm not sure we want to do anything with that... but TS requires us to have it */ | ||
async collectDefaultCompletions(_uri: string, _result: CompletionsCollector): Promise<void> {} | ||
|
||
private getContext(doc: AugmentedSourceCode): RequestContext { | ||
const context: RequestContext = { | ||
doc, | ||
}; | ||
|
||
if (doc.type === SourceCodeType.LiquidHtml && !(doc.ast instanceof Error)) { | ||
const schema = findSchemaNode(doc.ast); | ||
if (!schema) return SKIP_CONTRIBUTION; | ||
const jsonString = schema?.source.slice( | ||
schema.blockStartPosition.end, | ||
schema.blockEndPosition.start, | ||
); | ||
context.schema = schema; | ||
context.parsed = parseJSON(jsonString); | ||
} | ||
|
||
return context; | ||
} | ||
} |
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
35 changes: 35 additions & 0 deletions
35
packages/theme-language-server-common/src/json/RequestContext.ts
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,35 @@ | ||
import { LiquidHtmlNode, LiquidRawTag } from '@shopify/liquid-html-parser'; | ||
import { isError, JSONNode, SourceCodeType } from '@shopify/theme-check-common'; | ||
import { | ||
AugmentedJsonSourceCode, | ||
AugmentedLiquidSourceCode, | ||
AugmentedSourceCode, | ||
} from '../documents'; | ||
|
||
export type RequestContext = { | ||
doc: AugmentedSourceCode; | ||
schema?: LiquidRawTag; | ||
parsed?: any | Error; | ||
}; | ||
|
||
export type LiquidRequestContext = { | ||
doc: Omit<AugmentedLiquidSourceCode, 'ast'> & { ast: LiquidHtmlNode }; | ||
schema: LiquidRawTag; | ||
parsed: any; | ||
}; | ||
|
||
export type JSONRequestContext = { | ||
doc: Omit<AugmentedJsonSourceCode, 'ast'> & { ast: JSONNode }; | ||
}; | ||
|
||
export function isLiquidRequestContext(context: RequestContext): context is LiquidRequestContext { | ||
const { doc, schema, parsed } = context; | ||
return ( | ||
doc.type === SourceCodeType.LiquidHtml && !!schema && !isError(doc.ast) && !isError(parsed) | ||
); | ||
} | ||
|
||
export function isJSONRequestContext(context: RequestContext): context is JSONRequestContext { | ||
const { doc } = context; | ||
return doc.type === SourceCodeType.JSON && !isError(doc.ast); | ||
} |
150 changes: 0 additions & 150 deletions
150
packages/theme-language-server-common/src/json/SchemaTranslationContributions.ts
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.