Feature/reapply syntax highlighting onColorscheme autocmd (#2565)

* initial effort to get syntax highlighting to reapply on colorscheme reload

* remove lint check for whitespace inside template literals

* neaten reducer reset tidy up comment in bufferhighlight
so it appears in documentation dropdown

* Refactor token colors to pass token scopes as an array of strings
update affected tests

* fix imports in token colors

* pass in only visible tokens to the neovim token synchronizer

* fix lint errors

* fix another lint error

* revert token destructuring in reconciler

* notify redraw when tokencolors change in neovim editor

* update redux

* [wip] get token subset in synchronizeTokenColors

* make cache access more readable

* remove subset functionality from token synchronizer

* convert highlight cache to map so it can be cleared and reused easily

* do not split tokens on space char as this changes the way tokens are identified

* revert package json and yarn lock

* move private fields to avoid lint errors

* add onColorsChanged to neovim instance mock

* fix comments for token formatter function in token colors
This commit is contained in:
Akin 2018-10-19 10:27:27 +01:00 committed by GitHub
parent 27cef2a0dc
commit 65d0e20455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 215 additions and 124 deletions

View File

@ -16,8 +16,13 @@ export interface IBufferHighlightsUpdater {
clearHighlightsForLine(line: number): void
}
// Helper class to efficiently update
// buffer highlights in a batch.
/**
* Helper class to efficiently update
* buffer highlights in a batch
*
* @name BufferHighlightsUpdater
* @class
*/
export class BufferHighlightsUpdater implements IBufferHighlightsUpdater {
private _calls: any[] = []

View File

@ -315,19 +315,15 @@ export class NeovimEditor extends Editor implements Oni.Editor {
this._actions.setColors(updatedColors)
}
this._colors.onColorsChanged.subscribe(() => onColorsChanged())
this._colors.onColorsChanged.subscribe(onColorsChanged)
onColorsChanged()
const onTokenColorschanged = () => {
if (this._neovimInstance.isInitialized) {
this._neovimInstance.tokenColorSynchronizer.synchronizeTokenColors(
this._tokenColors.tokenColors,
)
}
}
this.trackDisposable(
this._tokenColors.onTokenColorsChanged.subscribe(() => onTokenColorschanged()),
this._tokenColors.onTokenColorsChanged.subscribe(() => {
if (this._neovimInstance.isInitialized) {
this._syntaxHighlighter.notifyColorschemeRedraw(`${this.activeBuffer.id}`)
}
}),
)
// Overlays
@ -1292,6 +1288,8 @@ export class NeovimEditor extends Editor implements Oni.Editor {
private async _onColorsChanged(): Promise<void> {
const newColorScheme = await this._neovimInstance.eval<string>("g:colors_name")
const { bufferNumber } = await this._neovimInstance.getContext()
this._syntaxHighlighter.notifyColorschemeRedraw(`${bufferNumber}`)
// In error cases, the neovim API layer returns an array
if (typeof newColorScheme !== "string") {

View File

@ -12,6 +12,7 @@ import { ISyntaxHighlightTokenInfo } from "./SyntaxHighlightingStore"
export interface ISyntaxHighlighter extends IDisposable {
notifyBufferUpdate(evt: Oni.EditorBufferChangedEventArgs): Promise<void>
notifyViewportChanged(bufferId: string, topLineInView: number, bottomLineInView: number): void
notifyColorschemeRedraw(id: string): void
getHighlightTokenAt(bufferId: string, position: types.Position): ISyntaxHighlightTokenInfo
}

View File

@ -4,12 +4,12 @@
* Handles enhanced syntax highlighting
*/
import { Buffer, Editor } from "oni-api"
import * as Log from "oni-core-logging"
import { prettyPrint } from "./../../Utility"
import { TokenColor, TokenColors } from "./../TokenColors"
import { NeovimEditor } from "./../../Editor/NeovimEditor"
import { HighlightInfo } from "./Definitions"
import {
ISyntaxHighlightLineInfo,
@ -18,21 +18,37 @@ import {
} from "./SyntaxHighlightingStore"
import { TokenScorer } from "./TokenScorer"
import { IBufferHighlightsUpdater } from "../../Editor/BufferHighlights"
import * as Selectors from "./SyntaxHighlightSelectors"
// SyntaxHighlightReconciler
//
// Essentially a renderer / reconciler, that will push
// highlight calls to the active buffer based on the active
// window and viewport
interface IBufferWithSyntaxHighlighter extends Buffer {
updateHighlights?: (
tokenColors: TokenColor[],
highlightCallback: (args: IBufferHighlightsUpdater) => void,
) => void
}
export interface IEditorWithSyntaxHighlighter extends Editor {
activeBuffer: IBufferWithSyntaxHighlighter
}
/**
* SyntaxHighlightReconciler
*
* Essentially a renderer / reconciler, that will push
* highlight calls to the active buffer based on the active
* window and viewport
* @name SyntaxHighlightReconciler
* @class
*/
export class SyntaxHighlightReconciler {
private _previousState: { [line: number]: ISyntaxHighlightLineInfo } = {}
private _tokenScorer = new TokenScorer()
constructor(private _editor: NeovimEditor, private _tokenColors: TokenColors) {}
constructor(private _editor: IEditorWithSyntaxHighlighter, private _tokenColors: TokenColors) {}
public update(state: ISyntaxHighlightState) {
const activeBuffer: any = this._editor.activeBuffer
const { activeBuffer } = this._editor
if (!activeBuffer) {
return
@ -77,6 +93,15 @@ export class SyntaxHighlightReconciler {
}
})
// Get only the token colors that apply to the visible section of the buffer
const visibleTokens = tokens.reduce((accumulator, { highlights }) => {
if (highlights) {
const tokenColors = highlights.map(({ tokenColor }) => tokenColor)
accumulator.push(...tokenColors)
}
return accumulator
}, [])
filteredLines.forEach(line => {
const lineNumber = parseInt(line, 10)
this._previousState[line] = Selectors.getLineFromBuffer(
@ -87,26 +112,22 @@ export class SyntaxHighlightReconciler {
if (tokens.length) {
Log.verbose(
"[SyntaxHighlightReconciler] Applying changes to " + tokens.length + " lines.",
`[SyntaxHighlightReconciler] Applying changes to ${tokens.length} lines.`,
)
activeBuffer.updateHighlights(
this._tokenColors.tokenColors,
(highlightUpdater: any) => {
tokens.forEach(token => {
const { line, highlights } = token
if (Log.isDebugLoggingEnabled()) {
Log.debug(
"[SyntaxHighlightingReconciler] Updating tokens for line: " +
token.line +
" | " +
JSON.stringify(highlights),
)
}
activeBuffer.updateHighlights(visibleTokens, highlightUpdater => {
tokens.forEach(token => {
const { line, highlights } = token
if (Log.isDebugLoggingEnabled()) {
Log.debug(
`[SyntaxHighlightingReconciler] Updating tokens for line: ${line} | ${prettyPrint(
highlights,
)}`,
)
}
highlightUpdater.setHighlightsForLine(line, highlights)
})
},
)
highlightUpdater.setHighlightsForLine(line, highlights)
})
})
}
}
}
@ -121,8 +142,10 @@ export class SyntaxHighlightReconciler {
}
private _getHighlightGroupFromScope(scopes: string[]): TokenColor {
const configurationColors = this._tokenColors.tokenColors
const highestRanked = this._tokenScorer.rankTokenScopes(scopes, configurationColors)
const highestRanked = this._tokenScorer.rankTokenScopes(
scopes,
this._tokenColors.tokenColors,
)
return highestRanked
}
}

View File

@ -28,7 +28,10 @@ import {
} from "./SyntaxHighlightingStore"
import { ISyntaxHighlighter } from "./ISyntaxHighlighter"
import { SyntaxHighlightReconciler } from "./SyntaxHighlightReconciler"
import {
IEditorWithSyntaxHighlighter,
SyntaxHighlightReconciler,
} from "./SyntaxHighlightReconciler"
import { getLineFromBuffer } from "./SyntaxHighlightSelectors"
import * as Utility from "./../../Utility"
@ -45,7 +48,10 @@ export class SyntaxHighlighter implements ISyntaxHighlighter {
constructor(private _editor: NeovimEditor, private _tokenColors: TokenColors) {
this._store = createSyntaxHighlightStore()
this._reconciler = new SyntaxHighlightReconciler(this._editor, this._tokenColors)
this._reconciler = new SyntaxHighlightReconciler(
this._editor as IEditorWithSyntaxHighlighter,
this._tokenColors,
)
this._unsubscribe = this._store.subscribe(() => {
const state = this._store.getState()
this._reconciler.update(state)
@ -62,12 +68,10 @@ export class SyntaxHighlighter implements ISyntaxHighlighter {
bottomLineInView: number,
): void {
Log.verbose(
"[SyntaxHighlighting.notifyViewportChanged] - bufferId: " +
bufferId +
" topLineInView: " +
topLineInView +
" bottomLineInView: " +
bottomLineInView,
`[SyntaxHighlighting.notifyViewportChanged] -
bufferId: ${bufferId}
topLineInView: ${topLineInView}
bottomLineInView: ${bottomLineInView}`,
)
const state = this._store.getState()
@ -89,6 +93,10 @@ export class SyntaxHighlighter implements ISyntaxHighlighter {
})
}
public async notifyColorschemeRedraw(bufferId: string) {
this._store.dispatch({ type: "SYNTAX_RESET_BUFFER", bufferId })
}
public async notifyBufferUpdate(evt: Oni.EditorBufferChangedEventArgs): Promise<void> {
const firstChange = evt.contentChanges[0]
if (!firstChange.range && !firstChange.rangeLength) {
@ -157,6 +165,10 @@ export class NullSyntaxHighlighter implements ISyntaxHighlighter {
return null
}
public notifyColorschemeRedraw(id: string): void {
return null
}
public notifyViewportChanged(
bufferId: string,
topLineInView: number,

View File

@ -51,6 +51,11 @@ export const bufferReducer: Reducer<IBufferSyntaxHighlightState> = (
action: ISyntaxHighlightAction,
) => {
switch (action.type) {
case "SYNTAX_RESET_BUFFER":
return {
...state,
lines: linesReducer(state.lines, action),
}
case "SYNTAX_UPDATE_BUFFER":
return {
...state,
@ -125,6 +130,21 @@ export const linesReducer: Reducer<SyntaxHighlightLines> = (
}
return newState
}
case "SYNTAX_RESET_BUFFER":
const resetState = Object.entries(state).reduce<SyntaxHighlightLines>(
(newResetState, [lineNumber, line]) => {
newResetState[lineNumber] = {
tokens: [],
ruleStack: null,
...line,
dirty: true,
}
return newResetState
},
{},
)
return resetState
case "SYNTAX_UPDATE_BUFFER":
const updatedBufferState: SyntaxHighlightLines = {
...state,

View File

@ -73,6 +73,10 @@ export const DefaultSyntaxHighlightState: ISyntaxHighlightState = {
}
export type ISyntaxHighlightAction =
| {
type: "SYNTAX_RESET_BUFFER"
bufferId: string
}
| {
type: "SYNTAX_UPDATE_BUFFER"
language: string
@ -150,14 +154,14 @@ const updateBufferLineMiddleware = (store: any) => (next: any) => (action: any)
action.lineNumber === 0 ? null : buffer.lines[action.lineNumber - 1].ruleStack
const tokenizeResult = grammar.tokenizeLine(action.line, previousRuleStack)
const tokens = tokenizeResult.tokens.map((t: any) => ({
const tokens = tokenizeResult.tokens.map(token => ({
range: types.Range.create(
action.lineNumber,
t.startIndex,
token.startIndex,
action.lineNumber,
t.endIndex,
token.endIndex,
),
scopes: t.scopes,
scopes: token.scopes,
}))
const updateInsertLineAction: ISyntaxHighlightAction = {
@ -180,7 +184,11 @@ const updateBufferLineMiddleware = (store: any) => (next: any) => (action: any)
const updateTokenMiddleware = (store: any) => (next: any) => (action: any) => {
const result: ISyntaxHighlightAction = next(action)
if (action.type === "SYNTAX_UPDATE_BUFFER" || action.type === "SYNTAX_UPDATE_BUFFER_VIEWPORT") {
if (
action.type === "SYNTAX_UPDATE_BUFFER" ||
action.type === "SYNTAX_UPDATE_BUFFER_VIEWPORT" ||
action.type === "SYNTAX_RESET_BUFFER"
) {
const state: ISyntaxHighlightState = store.getState()
const bufferId = action.bufferId
@ -212,7 +220,7 @@ const updateTokenMiddleware = (store: any) => (next: any) => (action: any) => {
syntaxHighlightingJobs.startJob(
new SyntaxHighlightingPeriodicJob(
store as any,
store,
action.bufferId,
grammar,
relevantRange.top,

View File

@ -119,7 +119,7 @@ export class TokenScorer {
if (parts.length < 2) {
return null
}
const matchingToken = theme.find(color => color.scope === scope)
const matchingToken = theme.find(color => color.scope.includes(scope))
if (matchingToken) {
return matchingToken
}

View File

@ -190,8 +190,8 @@ class TokenThemeProvider extends React.Component<IProps, IState> {
public generateTokens({ defaultMap = defaultsToMap, defaultTokens }: IGenerateTokenArgs) {
const newTokens = Object.keys(defaultMap).reduce((acc, defaultTokenName) => {
const defaultToken = this.props.tokenColors.find(
token => token.scope === defaultTokenName,
const defaultToken = this.props.tokenColors.find(token =>
token.scope.includes(defaultTokenName),
)
if (defaultToken) {
const tokens = defaultMap[defaultTokenName].map(name =>

View File

@ -8,8 +8,11 @@
import { Event, IDisposable, IEvent } from "oni-types"
import { Configuration, IConfigurationValues } from "./Configuration"
import { ThemeManager } from "./Themes"
export interface TokenColor {
scope: string
scope: string[]
settings: TokenColorStyle
// private field for determining where a token came from
_source?: string
@ -18,6 +21,7 @@ export interface TokenColor {
export interface ThemeToken {
scope: string | string[]
settings: TokenColorStyle
_source?: string
}
export interface TokenColorStyle {
@ -27,9 +31,6 @@ export interface TokenColorStyle {
fontStyle: "bold" | "italic" | "bold italic"
}
import { Configuration, IConfigurationValues } from "./Configuration"
import { ThemeManager } from "./Themes"
export class TokenColors implements IDisposable {
private _subscriptions: IDisposable[] = []
private _tokenColors: TokenColor[] = []
@ -71,38 +72,49 @@ export class TokenColors implements IDisposable {
this._subscriptions = []
}
private _flattenThemeTokens = (themeTokens: ThemeToken[] = []) => {
const multidimensionalTokens = themeTokens.map(token => {
if (Array.isArray(token.scope)) {
return token.scope.map(s => ({
scope: s,
settings: token.settings,
}))
}
return token
})
return [].concat(...multidimensionalTokens).filter(t => !!t.scope)
}
private _updateTokenColors(): void {
const {
activeTheme: {
colors: { "editor.tokenColors": tokenColorsFromTheme = [] },
colors: { "editor.tokenColors": themeTokens = [] },
},
} = this._themeManager
const themeTokens = this._flattenThemeTokens(tokenColorsFromTheme)
const userColors = this._configuration.getValue("editor.tokenColors")
this._tokenColors = this._mergeTokenColors({
const combinedColors = this._mergeTokenColors({
user: userColors,
theme: themeTokens,
defaults: this._defaultTokenColors,
})
this._tokenColors = this._convertThemeTokenScopes(combinedColors)
this._onTokenColorsChangedEvent.dispatch()
}
/**
* Theme tokens can pass in token scopes as a string or an array
* this converts all token scopes passed in to an array of strings
*
* @name convertThemeTokenScopes
* @function
* @param {ThemeToken[]} tokens
* @returns {TokenColor[]}
*/
private _convertThemeTokenScopes(tokens: ThemeToken[]) {
// TODO: figure out how space separated token scopes should be handled
// token.scope.split(" ") -> convert "meta.var string.quoted" -> ["meta.var", "string.quoted"]
// this however breaks prioritisation of tokens
return tokens.map(token => {
const scope = !token.scope
? []
: Array.isArray(token.scope)
? token.scope
: [token.scope]
return { ...token, scope }
})
}
/**
* Merge different token source whilst unifying settings
* each source is passed by name so that later the priority
@ -111,34 +123,32 @@ export class TokenColors implements IDisposable {
* user source
*/
private _mergeTokenColors(tokens: {
user: TokenColor[]
user: ThemeToken[]
defaults: TokenColor[]
theme: TokenColor[]
theme: ThemeToken[]
}) {
return Object.keys(tokens).reduce(
(output, key) => {
const tokenColors: TokenColor[] = tokens[key]
return tokenColors.reduce((mergedTokens, currentToken) => {
return Object.entries(tokens).reduce<ThemeToken[]>(
(output, [_source, tokenColors]) =>
tokenColors.reduce((mergedTokens, currentToken) => {
const duplicateToken = mergedTokens.find(t => currentToken.scope === t.scope)
if (duplicateToken) {
return mergedTokens.map(existingToken => {
if (existingToken.scope === duplicateToken.scope) {
return this._mergeSettings(existingToken, {
...currentToken,
_source: key,
_source,
})
}
return existingToken
})
}
return [...mergedTokens, { ...currentToken, _source: key }]
}, output)
},
[] as TokenColor[],
return [...mergedTokens, { ...currentToken, _source }]
}, output),
[],
)
}
private _mergeSettings(prev: TokenColor, next: TokenColor) {
private _mergeSettings(prev: ThemeToken, next: ThemeToken) {
const priority = {
user: 2,
theme: 1,

View File

@ -311,3 +311,8 @@ export function get<T>(obj: T, ...paths: string[]): string {
.split(".")
.reduce((a, b) => (a && a[b] ? a[b] : null), obj)
}
export function prettyPrint<T extends object>(item: T, spacing = 2) {
// tslint:disable-next-line
console.log(JSON.stringify(item, null, spacing))
}

View File

@ -524,8 +524,8 @@ export class NeovimInstance extends EventEmitter implements INeovimInstance {
const settings = vimHighlightToTokenColorStyle(currentValue)
const newScopeNames: string[] = VimHighlightToDefaultScope[highlightGroupName] || []
const newScopes = newScopeNames.map((scope): TokenColor => ({
scope,
const newScopes = newScopeNames.map(scope => ({
scope: [scope],
settings,
}))

View File

@ -28,42 +28,41 @@ const getGuiStringFromTokenColor = ({ settings: { fontStyle } }: TokenColor): st
}
export class NeovimTokenColorSynchronizer {
private _currentIndex: number = 0
private _currentIndex = 0
private _tokenScopeSelectorToHighlightName: { [key: string]: string } = {}
private _highlightNameToHighlightValue: { [key: string]: string } = {}
private _highlightNameToHighlightValue = new Map<string, string>()
constructor(private _neovimInstance: NeovimInstance) {}
constructor(private _neovimInstance: NeovimInstance) {
this._neovimInstance.onColorsChanged.subscribe(this._resetHighlightCache)
}
// This method creates highlight groups for any token colors that haven't been set yet
public async synchronizeTokenColors(tokenColors: TokenColor[]): Promise<void> {
const highlightsToAdd = tokenColors.map(tokenColor => {
public async synchronizeTokenColors(tokenColors: TokenColor[]) {
const highlightsToAdd = tokenColors.reduce<string[]>((newHighlights, tokenColor) => {
const highlightName = this._getOrCreateHighlightGroup(tokenColor)
const highlightFromScope = this._convertTokenStyleToHighlightInfo(tokenColor)
const currentHighlight = this._highlightNameToHighlightValue[highlightName]
const currentHighlight = this._highlightNameToHighlightValue.get(highlightName)
if (currentHighlight === highlightFromScope) {
return null
} else {
this._highlightNameToHighlightValue[highlightName] = highlightFromScope
return highlightFromScope
return newHighlights
}
})
this._highlightNameToHighlightValue.set(highlightName, highlightFromScope)
return [...newHighlights, highlightFromScope]
}, [])
const filteredHighlights = highlightsToAdd.filter(hl => !!hl)
const atomicCalls = filteredHighlights.map(hlCommand => ["nvim_command", [hlCommand]])
const atomicCalls = highlightsToAdd.map(hlCommand => ["nvim_command", [hlCommand]])
if (!atomicCalls.length) {
return
}
Log.info(
"[NeovimTokenColorSynchronizer::synchronizeTokenColors] Setting " +
atomicCalls.length +
" highlights",
`[NeovimTokenColorSynchronizer::synchronizeTokenColors] Setting ${
atomicCalls.length
} highlights`,
)
await this._neovimInstance.request("nvim_call_atomic", [atomicCalls])
this._neovimInstance.request("nvim_call_atomic", [atomicCalls])
Log.info(
"[NeovimTokenColorSynchronizer::synchronizeTokenColors] Highlights set successfully",
)
@ -77,6 +76,10 @@ export class NeovimTokenColorSynchronizer {
return this._getOrCreateHighlightGroup(tokenColor)
}
private _resetHighlightCache = () => {
this._highlightNameToHighlightValue.clear()
}
private _convertTokenStyleToHighlightInfo(tokenColor: TokenColor): string {
const name = this._getOrCreateHighlightGroup(tokenColor)
const foregroundColor = Color(tokenColor.settings.foreground).hex()
@ -86,9 +89,9 @@ export class NeovimTokenColorSynchronizer {
}
private _getOrCreateHighlightGroup(tokenColor: TokenColor): string {
const existingGroup = this._tokenScopeSelectorToHighlightName[
this._getKeyFromTokenColor(tokenColor)
]
const tokenKey = this._getKeyFromTokenColor(tokenColor)
const existingGroup = this._tokenScopeSelectorToHighlightName[tokenKey]
if (existingGroup) {
return existingGroup
} else {
@ -98,21 +101,20 @@ export class NeovimTokenColorSynchronizer {
"[NeovimTokenColorSynchronizer::_getOrCreateHighlightGroup] Creating new highlight group - " +
newHighlightGroupName,
)
this._tokenScopeSelectorToHighlightName[
this._getKeyFromTokenColor(tokenColor)
] = newHighlightGroupName
this._tokenScopeSelectorToHighlightName[tokenKey] = newHighlightGroupName
return newHighlightGroupName
}
}
private _getKeyFromTokenColor(tokenColor: TokenColor): string {
const {
settings: { background, foreground, fontStyle },
settings: { background = "none", foreground = "none", fontStyle = "none" },
} = tokenColor
const separator = `__`
const bg = `background-${background}`
const fg = `foreground-${foreground}`
const bold = `bold-${fontStyle && fontStyle.includes("bold")}`
const italic = `italic-${fontStyle && fontStyle.includes("italic")}`
return `${tokenColor.scope}_${bg}_${fg}_${bold}_${italic}`
const bold = `bold-${fontStyle ? fontStyle.includes("bold") : false}`
const italic = `italic-${fontStyle ? fontStyle.includes("italic") : false}`
return [tokenColor.scope, bg, fg, bold, italic].join(separator)
}
}

View File

@ -16,6 +16,12 @@ export class MockNeovimInstance {
private _requests: NeovimRequest[] = []
private _pendingPromises: Array<Utility.ICompletablePromise<any>> = []
public get onColorsChanged() {
return {
subscribe: (fn: (args?: any) => any) => fn(),
}
}
public request(requestName: string, args: any[]) {
this._requests.push({ requestName, args })
const promise = Utility.createCompletablePromise()

View File

@ -25,7 +25,7 @@ describe("SyntaxHighlightReconciler", () => {
mockTokenColors = new Mocks.MockTokenColors([
{
scope: "scope.test",
scope: ["scope.test"],
settings: {
background: COLOR_BLACK,
foreground: COLOR_WHITE,
@ -90,7 +90,7 @@ describe("SyntaxHighlightReconciler", () => {
{
range: types.Range.create(0, 0, 0, 5),
tokenColor: {
scope: "scope.test",
scope: ["scope.test"],
settings: {
background: COLOR_BLACK,
foreground: COLOR_WHITE,
@ -151,7 +151,7 @@ describe("SyntaxHighlightReconciler", () => {
{
range: types.Range.create(0, 0, 0, 5),
tokenColor: {
scope: "scope.test",
scope: ["scope.test"],
settings: {
background: COLOR_BLACK,
foreground: COLOR_WHITE,

View File

@ -9,7 +9,7 @@ import { TokenColor } from "./../../src/Services/TokenColors"
import { MockNeovimInstance } from "./../Mocks/neovim"
const createTokenColor = (scope: string): TokenColor => ({
const createTokenColor = (...scope: string[]): TokenColor => ({
scope,
settings: {
foreground: "#FFFFFF",

View File

@ -25,6 +25,7 @@
"check-format"
],
"whitespace": false,
"no-trailing-whitespace": [true, "ignore-template-strings"],
"trailing-comma": [
true,
{