Compare commits

...

1 Commits

Author SHA1 Message Date
86ae7a989f initial go
Signed-off-by: Jess Frazelle <github@jessfraz.com>

fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

better

Signed-off-by: Jess Frazelle <github@jessfraz.com>

typo

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2025-05-14 19:09:38 -07:00
12 changed files with 375 additions and 346 deletions

View File

@ -834,7 +834,13 @@ test('theme persists', async ({ page, context }) => {
})
test.describe('code color goober', { tag: '@snapshot' }, () => {
test('code color goober', async ({ page, context, scene, cmdBar }) => {
test('code color goober', async ({
page,
context,
scene,
cmdBar,
editor,
}) => {
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem(
@ -879,6 +885,7 @@ sweepSketch = startSketchOn(XY)
context,
scene,
cmdBar,
editor,
}) => {
const u = await getUtils(page)
await context.addInitScript(async () => {

View File

@ -41,6 +41,14 @@ interface LSPRequestMap {
LSP.DefinitionParams,
LSP.Definition | LSP.DefinitionLink[] | null,
]
'textDocument/documentColor': [
LSP.DocumentColorParams,
LSP.ColorInformation[] | null,
]
'textDocument/colorPresentation': [
LSP.ColorPresentationParams,
LSP.ColorPresentation[] | null,
]
}
// Client to server
@ -229,6 +237,22 @@ export class LanguageServerClient {
return await this.request('textDocument/definition', params)
}
async textDocumentDocumentColor(params: LSP.DocumentColorParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.colorProvider) {
return null
}
return await this.request('textDocument/documentColor', params)
}
async textDocumentColorPresentation(params: LSP.ColorPresentationParams) {
const serverCapabilities = this.getServerCapabilities()
if (!serverCapabilities.colorProvider) {
return null
}
return await this.request('textDocument/colorPresentation', params)
}
attachPlugin(plugin: LanguageServerPlugin) {
this.plugins.push(plugin)
}

View File

@ -21,6 +21,7 @@ export {
lspRenameEvent,
lspSemanticTokensEvent,
lspCodeActionEvent,
lspColorUpdateEvent,
} from './plugin/annotation'
export {
LanguageServerPlugin,

View File

@ -6,6 +6,7 @@ export enum LspAnnotation {
Diagnostics = 'diagnostics',
Rename = 'rename',
CodeAction = 'code-action',
ColorUpdate = 'color-update',
}
const lspEvent = Annotation.define<LspAnnotation>()
@ -14,3 +15,4 @@ export const lspFormatCodeEvent = lspEvent.of(LspAnnotation.FormatCode)
export const lspDiagnosticsEvent = lspEvent.of(LspAnnotation.Diagnostics)
export const lspRenameEvent = lspEvent.of(LspAnnotation.Rename)
export const lspCodeActionEvent = lspEvent.of(LspAnnotation.CodeAction)
export const lspColorUpdateEvent = lspEvent.of(LspAnnotation.ColorUpdate)

View File

@ -0,0 +1,278 @@
import {
StateEffect,
StateField,
type Extension,
type Range,
} from '@codemirror/state'
import {
Decoration,
type DecorationSet,
EditorView,
ViewPlugin,
WidgetType,
type ViewUpdate,
} from '@codemirror/view'
import type { LanguageServerPlugin } from './lsp'
import { lspColorUpdateEvent } from './annotation'
import { isArray } from '../lib/utils'
import { offsetToPos, posToOffset, posToOffsetOrZero } from './util'
import type * as LSP from 'vscode-languageserver-protocol'
/* ------------------------------------------------------------------ */
/* ---------- original helpers / widget / color utilities ---------- */
/* ------------------------------------------------------------------ */
interface PickerState {
from: number
to: number
red: number
green: number
blue: number
alpha: number
}
export interface WidgetOptions extends PickerState {
color: string
}
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
function rgbaToHex(color: LSP.Color): string {
return (
'#' +
[color.red, color.green, color.blue]
.map((c) =>
Math.round(c * 255)
.toString(16)
.padStart(2, '0')
)
.join('')
)
}
function hexToRGBComponents(hex: string): number[] {
const r = hex.slice(1, 3)
const g = hex.slice(3, 5)
const b = hex.slice(5, 7)
return [parseInt(r, 16) / 255, parseInt(g, 16) / 255, parseInt(b, 16) / 255]
}
async function discoverColorsViaLsp(
view: EditorView,
plugin: LanguageServerPlugin
): Promise<WidgetOptions | Array<WidgetOptions> | null> {
const responses = await plugin.requestDocumentColors()
if (!responses) return null
const colors: Array<WidgetOptions> = []
for (const color of responses) {
if (!color.range || !color.color) continue
const { start, end } = color.range
const from = posToOffset(view.state.doc, start)
const to = posToOffset(view.state.doc, end)
if (from == null || to == null) continue
colors.push({
color: rgbaToHex(color.color),
...color.color,
from,
to,
})
}
return colors
}
async function colorPickersDecorations(
view: EditorView,
plugin: LanguageServerPlugin
): Promise<DecorationSet> {
const widgets: Array<Range<Decoration>> = []
const maybe = await discoverColorsViaLsp(view, plugin)
if (!maybe) return Decoration.none
const optionsList = isArray(maybe) ? maybe : [maybe]
for (const wo of optionsList) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(wo),
side: 1,
}).range(wo.from)
)
}
return Decoration.set(widgets)
}
export const wrapperClassName = 'cm-css-color-picker-wrapper'
class ColorPickerWidget extends WidgetType {
private readonly state: PickerState
private readonly color: string
constructor({ color, ...state }: WidgetOptions) {
super()
this.state = state
this.color = color
}
eq(other: ColorPickerWidget) {
return (
other.color === this.color &&
other.state.from === this.state.from &&
other.state.to === this.state.to &&
other.state.alpha === this.state.alpha
)
}
toDOM() {
const picker = document.createElement('input')
pickerState.set(picker, this.state)
picker.type = 'color'
picker.value = this.color
const wrapper = document.createElement('span')
wrapper.appendChild(picker)
wrapper.className = wrapperClassName
return wrapper
}
ignoreEvent() {
return false
}
}
export const colorPickerTheme = EditorView.baseTheme({
[`.${wrapperClassName}`]: {
display: 'inline-block',
outline: '1px solid #eee',
marginRight: '0.6ch',
height: '1em',
width: '1em',
transform: 'translateY(1px)',
},
[`.${wrapperClassName} input[type="color"]`]: {
cursor: 'pointer',
height: '100%',
width: '100%',
padding: 0,
border: 'none',
'&::-webkit-color-swatch-wrapper': { padding: 0 },
'&::-webkit-color-swatch': { border: 'none' },
'&::-moz-color-swatch': { border: 'none' },
},
})
/* ------------------------------------------------------------------ */
/* ------------------- ✅ new state machinery -------------------- */
/* ------------------------------------------------------------------ */
// Effect that carries a fresh DecorationSet
const setColorDecorations = StateEffect.define<DecorationSet>()
// Field that stores the current DecorationSet
const colorDecorationsField = StateField.define<DecorationSet>({
create: () => Decoration.none,
update(value, tr) {
value = value.map(tr.changes)
for (const e of tr.effects) if (e.is(setColorDecorations)) value = e.value
return value
},
provide: (f) => EditorView.decorations.from(f),
})
/* ------------------------------------------------------------------ */
/* ------------------ original ViewPlugin, patched ---------------- */
/* ------------------------------------------------------------------ */
export const makeColorPicker = (plugin: ViewPlugin<LanguageServerPlugin>) =>
ViewPlugin.fromClass(
class ColorPickerViewPlugin {
plugin: LanguageServerPlugin | null
constructor(view: EditorView) {
this.plugin = view.plugin(plugin)
if (!this.plugin) return
// initial async load → dispatch decorations
// eslint-disable-next-line @typescript-eslint/no-floating-promises
colorPickersDecorations(view, this.plugin).then((deco) => {
view.dispatch({ effects: setColorDecorations.of(deco) })
})
}
async update(update: ViewUpdate) {
if (!this.plugin) return
if (!(update.docChanged || update.viewportChanged)) return
const deco = await colorPickersDecorations(update.view, this.plugin)
update.view.dispatch({ effects: setColorDecorations.of(deco) })
}
},
{
eventHandlers: {
change: (e: Event, view: EditorView) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
colorPickerChange(e, view, plugin)
},
},
}
)
/* ------------------------------------------------------------------ */
/* -------------------- unchanged event handler ------------------- */
/* ------------------------------------------------------------------ */
async function colorPickerChange(
e: Event,
view: EditorView,
plugin: ViewPlugin<LanguageServerPlugin>
): Promise<boolean> {
const value = view.plugin(plugin)
if (!value) return false
const target = e.target as HTMLInputElement
if (
target.nodeName !== 'INPUT' ||
!target.parentElement?.classList.contains(wrapperClassName)
)
return false
const data = pickerState.get(target)!
const converted = target.value + data.alpha
const [red, green, blue] = hexToRGBComponents(converted)
const responses = await value.requestColorPresentation(
{ red, green, blue, alpha: data.alpha },
{
start: offsetToPos(view.state.doc, data.from),
end: offsetToPos(view.state.doc, data.to),
}
)
if (!responses?.length) return false
for (const resp of responses) {
const changes = resp.textEdit
? {
from: posToOffsetOrZero(view.state.doc, resp.textEdit.range.start),
to: posToOffsetOrZero(view.state.doc, resp.textEdit.range.end),
insert: resp.textEdit.newText,
}
: { from: data.from, to: data.to, insert: resp.label }
view.dispatch({ changes, annotations: [lspColorUpdateEvent] })
}
return true
}
/* ------------------------------------------------------------------ */
/* ------------------------- public API --------------------------- */
/* ------------------------------------------------------------------ */
export default function lspColorsExt(
plugin: ViewPlugin<LanguageServerPlugin>
): Extension {
return [colorDecorationsField, makeColorPicker(plugin), colorPickerTheme]
}

View File

@ -48,6 +48,7 @@ import { isArray } from '../lib/utils'
import lspGoToDefinitionExt from './go-to-definition'
import lspRenameExt from './rename'
import lspSignatureHelpExt from './signature-help'
import lspColorsExt from './colors'
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, '')
export const docPathFacet = Facet.define<string, string>({
@ -534,6 +535,37 @@ export class LanguageServerPlugin implements PluginValue {
})
}
async requestDocumentColors() {
if (
!(this.client.getServerCapabilities().colorProvider && this.client.ready)
) {
return
}
const result = await this.client.textDocumentDocumentColor({
textDocument: { uri: this.getDocUri() },
})
if (!result) return
return result
}
async requestColorPresentation(color: LSP.Color, range: LSP.Range) {
if (
!(this.client.getServerCapabilities().colorProvider && this.client.ready)
) {
return
}
const result = await this.client.textDocumentColorPresentation({
textDocument: { uri: this.getDocUri() },
color,
range,
})
if (!result) return
return result
}
async requestRename(
view: EditorView,
{ line, character }: { line: number; character: number }
@ -1318,6 +1350,7 @@ export class LanguageServerPluginSpec
return [
linter(null),
lspAutocompleteExt(plugin),
lspColorsExt(plugin),
lspFormatExt(plugin),
lspGoToDefinitionExt(plugin),
lspHoverExt(plugin),

14
rust/Cargo.lock generated
View File

@ -535,7 +535,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -963,7 +963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1746,7 +1746,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -2987,7 +2987,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3306,7 +3306,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -3900,7 +3900,7 @@ dependencies = [
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -4753,7 +4753,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@ -4220,8 +4220,8 @@ sketch001 = startSketchOn(XY)
result,
vec![tower_lsp::lsp_types::ColorInformation {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,
@ -4272,8 +4272,8 @@ sketch001 = startSketchOn(XY)
result,
vec![tower_lsp::lsp_types::ColorInformation {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,
@ -4291,8 +4291,8 @@ sketch001 = startSketchOn(XY)
uri: "file:///test.kcl".try_into().unwrap(),
},
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
start: tower_lsp::lsp_types::Position { line: 4, character: 25 },
end: tower_lsp::lsp_types::Position { line: 4, character: 32 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,

View File

@ -438,8 +438,15 @@ impl Node<Program> {
let add_color = |literal: &Node<Literal>| {
// Check if the string is a color.
if let Some(c) = literal.value.is_color() {
let source_range = literal.as_source_range();
// We subtract 1 from either side because of the "'s in the literal.
let fixed_source_range = SourceRange::new(
source_range.start() + 1,
source_range.end() - 1,
source_range.module_id(),
);
let color = ColorInformation {
range: literal.as_source_range().to_lsp_range(code),
range: fixed_source_range.to_lsp_range(code),
color: tower_lsp::lsp_types::Color {
red: c.r,
green: c.g,
@ -498,7 +505,11 @@ impl Node<Program> {
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
match node {
crate::walk::Node::Literal(literal) => {
if literal.start == pos_start && literal.end == pos_end && literal.value.is_color().is_some() {
// Account for the quotes in the literal.
if (literal.start + 1) == pos_start
&& (literal.end - 1) == pos_end
&& literal.value.is_color().is_some()
{
found.replace(true);
return Ok(true);
}

View File

@ -1,328 +0,0 @@
import { language, syntaxTree } from '@codemirror/language'
import type { Extension, Range, Text } from '@codemirror/state'
import type { DecorationSet, ViewUpdate } from '@codemirror/view'
import {
Decoration,
EditorView,
ViewPlugin,
WidgetType,
} from '@codemirror/view'
import type { Tree } from '@lezer/common'
import { NodeProp } from '@lezer/common'
import { isArray } from '@src/lib/utils'
interface PickerState {
from: number
to: number
alpha: string
colorType: ColorType
}
export interface WidgetOptions extends PickerState {
color: string
}
export type ColorData = Omit<WidgetOptions, 'from' | 'to'>
const pickerState = new WeakMap<HTMLInputElement, PickerState>()
export enum ColorType {
hex = 'HEX',
}
const hexRegex = /(^|\b)(#[0-9a-f]{3,9})(\b|$)/i
function discoverColorsInKCL(
syntaxTree: Tree,
from: number,
to: number,
typeName: string,
doc: Text,
language?: string
): WidgetOptions | Array<WidgetOptions> | null {
switch (typeName) {
case 'Program':
case 'VariableDeclaration':
case 'CallExpressionKw':
case 'ObjectExpression':
case 'ObjectProperty':
case 'ArgumentList':
case 'PipeExpression': {
let innerTree = syntaxTree.resolveInner(from, 0).tree
if (!innerTree) {
innerTree = syntaxTree.resolveInner(from, 1).tree
if (!innerTree) {
return null
}
}
const overlayTree = innerTree.prop(NodeProp.mounted)?.tree
if (overlayTree?.type.name !== 'Styles') {
return null
}
const ret: Array<WidgetOptions> = []
overlayTree.iterate({
from: 0,
to: overlayTree.length,
enter: ({ type, from: overlayFrom, to: overlayTo }) => {
const maybeWidgetOptions = discoverColorsInKCL(
syntaxTree,
// We add one because the tree doesn't include the
// quotation mark from the style tag
from + 1 + overlayFrom,
from + 1 + overlayTo,
type.name,
doc,
language
)
if (maybeWidgetOptions) {
if (isArray(maybeWidgetOptions)) {
console.error('Unexpected nested overlays')
ret.push(...maybeWidgetOptions)
} else {
ret.push(maybeWidgetOptions)
}
}
},
})
return ret
}
case 'String': {
const result = parseColorLiteral(doc.sliceString(from, to))
if (!result) {
return null
}
return {
...result,
from,
to,
}
}
default:
return null
}
}
export function parseColorLiteral(colorLiteral: string): ColorData | null {
const literal = colorLiteral.replace(/"/g, '')
const match = hexRegex.exec(literal)
if (!match) {
return null
}
const [color, alpha] = toFullHex(literal)
return {
colorType: ColorType.hex,
color,
alpha,
}
}
function colorPickersDecorations(
view: EditorView,
discoverColors: typeof discoverColorsInKCL
) {
const widgets: Array<Range<Decoration>> = []
const st = syntaxTree(view.state)
for (const range of view.visibleRanges) {
st.iterate({
from: range.from,
to: range.to,
enter: ({ type, from, to }) => {
const maybeWidgetOptions = discoverColors(
st,
from,
to,
type.name,
view.state.doc,
view.state.facet(language)?.name
)
if (!maybeWidgetOptions) {
return
}
if (!isArray(maybeWidgetOptions)) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(maybeWidgetOptions),
side: 1,
}).range(maybeWidgetOptions.from)
)
return
}
for (const wo of maybeWidgetOptions) {
widgets.push(
Decoration.widget({
widget: new ColorPickerWidget(wo),
side: 1,
}).range(wo.from)
)
}
},
})
}
return Decoration.set(widgets)
}
function toFullHex(color: string): string[] {
if (color.length === 4) {
// 3-char hex
return [
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
'',
]
}
if (color.length === 5) {
// 4-char hex (alpha)
return [
`#${color[1].repeat(2)}${color[2].repeat(2)}${color[3].repeat(2)}`,
color[4].repeat(2),
]
}
if (color.length === 9) {
// 8-char hex (alpha)
return [`#${color.slice(1, -2)}`, color.slice(-2)]
}
return [color, '']
}
export const wrapperClassName = 'cm-css-color-picker-wrapper'
class ColorPickerWidget extends WidgetType {
private readonly state: PickerState
private readonly color: string
constructor({ color, ...state }: WidgetOptions) {
super()
this.state = state
this.color = color
}
eq(other: ColorPickerWidget) {
return (
other.state.colorType === this.state.colorType &&
other.color === this.color &&
other.state.from === this.state.from &&
other.state.to === this.state.to &&
other.state.alpha === this.state.alpha
)
}
toDOM() {
const picker = document.createElement('input')
pickerState.set(picker, this.state)
picker.type = 'color'
picker.value = this.color
const wrapper = document.createElement('span')
wrapper.appendChild(picker)
wrapper.className = wrapperClassName
return wrapper
}
ignoreEvent() {
return false
}
}
export const colorPickerTheme = EditorView.baseTheme({
[`.${wrapperClassName}`]: {
display: 'inline-block',
outline: '1px solid #eee',
marginRight: '0.6ch',
height: '1em',
width: '1em',
transform: 'translateY(1px)',
},
[`.${wrapperClassName} input[type="color"]`]: {
cursor: 'pointer',
height: '100%',
width: '100%',
padding: 0,
border: 'none',
'&::-webkit-color-swatch-wrapper': {
padding: 0,
},
'&::-webkit-color-swatch': {
border: 'none',
},
'&::-moz-color-swatch': {
border: 'none',
},
},
})
interface IFactoryOptions {
discoverColors: typeof discoverColorsInKCL
}
export const makeColorPicker = (options: IFactoryOptions) =>
ViewPlugin.fromClass(
class ColorPickerViewPlugin {
decorations: DecorationSet
constructor(view: EditorView) {
this.decorations = colorPickersDecorations(view, options.discoverColors)
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = colorPickersDecorations(
update.view,
options.discoverColors
)
}
}
},
{
decorations: (v) => v.decorations,
eventHandlers: {
change: (e, view) => {
const target = e.target as HTMLInputElement
if (
target.nodeName !== 'INPUT' ||
!target.parentElement ||
!target.parentElement.classList.contains(wrapperClassName)
) {
return false
}
const data = pickerState.get(target)!
let converted = '"' + target.value + data.alpha + '"'
view.dispatch({
changes: {
from: data.from,
to: data.to,
insert: converted,
},
})
return true
},
},
}
)
export const colorPicker: Extension = [
makeColorPicker({ discoverColors: discoverColorsInKCL }),
colorPickerTheme,
]

View File

@ -7,6 +7,7 @@ import type {
} from '@kittycad/codemirror-lsp-client'
import {
lspCodeActionEvent,
lspColorUpdateEvent,
lspFormatCodeEvent,
lspPlugin,
lspRenameEvent,
@ -88,6 +89,8 @@ export class KclPlugin implements PluginValue {
isRelevant = true
} else if (tr.annotation(lspCodeActionEvent.type)) {
isRelevant = true
} else if (tr.annotation(lspColorUpdateEvent.type)) {
isRelevant = true
}
// Don't make this an else.

View File

@ -8,7 +8,6 @@ import type {
import type * as LSP from 'vscode-languageserver-protocol'
import { kclPlugin } from '@src/editor/plugins/lsp/kcl'
import { colorPicker } from '@src/editor/plugins/lsp/kcl/colors'
export interface LanguageOptions {
workspaceFolders: LSP.WorkspaceFolder[]
@ -22,7 +21,6 @@ export interface LanguageOptions {
export function kcl(options: LanguageOptions) {
return new LanguageSupport(KclLanguage, [
colorPicker,
kclPlugin({
documentUri: options.documentUri,
workspaceFolders: options.workspaceFolders,