@stacksjs/defaults 0.75.62 → 0.75.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,228 +0,0 @@
1
- /**
2
- * Just enough of the `vscode` API for ./pickier.test.ts and ./env.test.ts:
3
- * documents, diagnostics, code actions, hovers and the provider registries,
4
- * recorded so tests can call the providers the way the editor would.
5
- */
6
-
7
- type Listener<T> = (event: T) => unknown
8
-
9
- export function emitter<T>() {
10
- const listeners = new Set<Listener<T>>()
11
- return {
12
- event: (listener: Listener<T>) => {
13
- listeners.add(listener)
14
- return { dispose: () => listeners.delete(listener) }
15
- },
16
- fire: (value: T) => {
17
- for (const listener of [...listeners]) listener(value)
18
- },
19
- }
20
- }
21
-
22
- export class Position {
23
- constructor(readonly line: number, readonly character: number) {}
24
- translate(lines = 0, characters = 0) {
25
- return new Position(this.line + lines, this.character + characters)
26
- }
27
- }
28
-
29
- export class Range {
30
- readonly start: Position
31
- readonly end: Position
32
- constructor(a: Position | number, b: Position | number, c?: number, d?: number) {
33
- this.start = typeof a === 'number' ? new Position(a, b as number) : a
34
- this.end = typeof a === 'number' ? new Position(c as number, d as number) : b as Position
35
- }
36
- }
37
-
38
- export class CodeActionKind {
39
- static readonly QuickFix = new CodeActionKind('quickfix')
40
- static readonly SourceFixAll = new CodeActionKind('source.fixAll')
41
- constructor(readonly value: string) {}
42
- append(part: string) {
43
- return new CodeActionKind(`${this.value}.${part}`)
44
- }
45
-
46
- contains(other: CodeActionKind) {
47
- return other.value === this.value || other.value.startsWith(`${this.value}.`)
48
- }
49
- }
50
-
51
- export class WorkspaceEdit {
52
- readonly edits: Array<{ uri: unknown, range?: Range, position?: Position, text: string }> = []
53
- replace(uri: unknown, range: Range, text: string) {
54
- this.edits.push({ uri, range, text })
55
- }
56
-
57
- insert(uri: unknown, position: Position, text: string) {
58
- this.edits.push({ uri, position, text })
59
- }
60
- }
61
-
62
- export class MarkdownString {
63
- value = ''
64
- appendMarkdown(text: string) {
65
- this.value += text
66
- return this
67
- }
68
-
69
- appendCodeblock(code: string, language = '') {
70
- this.value += `\n\`\`\`${language}\n${code}\n\`\`\`\n`
71
- return this
72
- }
73
- }
74
-
75
- export interface FakeDocument {
76
- uri: { fsPath: string, scheme: string, toString: () => string }
77
- languageId: string
78
- version: number
79
- isClosed: boolean
80
- lineCount: number
81
- getText: () => string
82
- lineAt: (line: number) => { text: string, lineNumber: number, range: Range }
83
- getWordRangeAtPosition: (position: Position) => Range | undefined
84
- }
85
-
86
- export function document(path: string, text: string, languageId = 'typescript'): FakeDocument {
87
- const lines = text.split('\n')
88
- return {
89
- uri: { fsPath: path, scheme: 'file', toString: () => `file://${path}` },
90
- languageId,
91
- version: 1,
92
- isClosed: false,
93
- lineCount: lines.length,
94
- getText: () => text,
95
- lineAt: (line: number) => ({ text: lines[line] ?? '', lineNumber: line, range: new Range(line, 0, line, (lines[line] ?? '').length) }),
96
- getWordRangeAtPosition: (position: Position) => {
97
- const line = lines[position.line] ?? ''
98
- const match = /\w+/.exec(line.slice(position.character))
99
- return match && match.index === 0 ? new Range(position.line, position.character, position.line, position.character + match[0].length) : undefined
100
- },
101
- }
102
- }
103
-
104
- export function fakeVscode(options: { folders: string[], settings?: Record<string, unknown>, documents?: FakeDocument[] }) {
105
- const settings = options.settings ?? {}
106
- const diagnostics = new Map<string, any[]>()
107
- const handlers = new Map<string, (...args: any[]) => any>()
108
- const codeActionProviders: any[] = []
109
- const formatters: any[] = []
110
- const hovers: any[] = []
111
- const messages: string[] = []
112
- const answers: Array<string | undefined> = []
113
- const inputs: any[] = []
114
- const output: string[] = []
115
- const applied: WorkspaceEdit[] = []
116
- const clipboard: string[] = []
117
- const opened = emitter<FakeDocument>()
118
- const changed = emitter<{ document: FakeDocument }>()
119
- const saved = emitter<FakeDocument>()
120
- const closed = emitter<FakeDocument>()
121
-
122
- const message = (text: string) => {
123
- messages.push(text)
124
- return Promise.resolve(answers.shift())
125
- }
126
-
127
- const api = {
128
- Position,
129
- Range,
130
- CodeActionKind,
131
- WorkspaceEdit,
132
- MarkdownString,
133
- DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 },
134
- Diagnostic: class {
135
- source?: string
136
- code?: string
137
- constructor(readonly range: Range, readonly message: string, readonly severity: number) {}
138
- },
139
- CodeAction: class {
140
- edit?: WorkspaceEdit
141
- diagnostics?: unknown[]
142
- constructor(readonly title: string, readonly kind: CodeActionKind) {}
143
- },
144
- Hover: class {
145
- constructor(readonly contents: MarkdownString, readonly range?: Range) {}
146
- },
147
- TextEdit: { replace: (range: Range, newText: string) => ({ range, newText }) },
148
- commands: {
149
- registerCommand: (id: string, handler: (...args: any[]) => any) => {
150
- handlers.set(id, handler)
151
- return { dispose: () => handlers.delete(id) }
152
- },
153
- },
154
- env: { clipboard: { writeText: async (text: string) => { clipboard.push(text) } } },
155
- languages: {
156
- createDiagnosticCollection: () => ({
157
- set: (uri: { toString: () => string }, list: any[]) => diagnostics.set(uri.toString(), list),
158
- delete: (uri: { toString: () => string }) => diagnostics.delete(uri.toString()),
159
- clear: () => diagnostics.clear(),
160
- dispose: () => {},
161
- }),
162
- registerCodeActionsProvider: (_selector: unknown, provider: unknown) => {
163
- codeActionProviders.push(provider)
164
- return { dispose: () => {} }
165
- },
166
- registerDocumentFormattingEditProvider: (_selector: unknown, provider: unknown) => {
167
- formatters.push(provider)
168
- return { dispose: () => {} }
169
- },
170
- registerHoverProvider: (_selector: unknown, provider: unknown) => {
171
- hovers.push(provider)
172
- return { dispose: () => {} }
173
- },
174
- },
175
- window: {
176
- activeTextEditor: undefined as undefined | { document: FakeDocument },
177
- createOutputChannel: () => ({ appendLine: (line: string) => output.push(line), dispose: () => {} }),
178
- showInformationMessage: message,
179
- showWarningMessage: message,
180
- showErrorMessage: message,
181
- showInputBox: (inputOptions: unknown) => {
182
- inputs.push(inputOptions)
183
- return Promise.resolve(answers.shift())
184
- },
185
- showQuickPick: (items: string[], pickOptions: unknown) => {
186
- inputs.push({ items, ...pickOptions as object })
187
- return Promise.resolve(answers.shift())
188
- },
189
- },
190
- workspace: {
191
- workspaceFolders: options.folders.map((folder, index) => ({ uri: { fsPath: folder }, name: folder, index })),
192
- textDocuments: options.documents ?? [],
193
- getConfiguration: () => ({ get: (key: string, fallback: unknown) => key in settings ? settings[key] : fallback }),
194
- getWorkspaceFolder: (uri: { fsPath: string }) => {
195
- const folder = options.folders.find(candidate => uri.fsPath.startsWith(`${candidate}/`))
196
- return folder ? { uri: { fsPath: folder } } : undefined
197
- },
198
- applyEdit: async (edit: WorkspaceEdit) => {
199
- applied.push(edit)
200
- return true
201
- },
202
- onDidOpenTextDocument: opened.event,
203
- onDidChangeTextDocument: changed.event,
204
- onDidSaveTextDocument: saved.event,
205
- onDidCloseTextDocument: closed.event,
206
- onDidChangeConfiguration: emitter<any>().event,
207
- },
208
- }
209
-
210
- return {
211
- api,
212
- diagnostics,
213
- handlers,
214
- codeActionProviders,
215
- formatters,
216
- hovers,
217
- messages,
218
- answers,
219
- inputs,
220
- output,
221
- applied,
222
- clipboard,
223
- events: { opened, changed, saved, closed },
224
- context: { subscriptions: [] as Array<{ dispose: () => unknown }> },
225
- }
226
- }
227
-
228
- export const flush = () => new Promise(resolve => setTimeout(resolve, 10))
@@ -1,141 +0,0 @@
1
- /**
2
- * The extension manifest.
3
- *
4
- * The Stacks extension is self-contained: stx support, pickier and env files
5
- * are built in, so it installs no extension pack and depends on no other
6
- * extension. stx support comes from `@stacksjs/stx-vscode`, the library build
7
- * of the stx extension (Stacks.vscode-stx); with both installed, the stx
8
- * extension stands down (stacksjs/stx#2020).
9
- */
10
- import { describe, expect, it } from 'bun:test'
11
- import { readFileSync } from 'node:fs'
12
- import { join } from 'node:path'
13
- import { libraryContributes, OWN_COMMAND_PREFIX, withStxContributes } from '../scripts/stx-contributes'
14
-
15
- const root = join(import.meta.dir, '..')
16
- const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
17
- const STX_EXTENSION_ID = 'Stacks.vscode-stx'
18
- const projectSettings = Bun.JSONC.parse(readFileSync(join(root, '.vscode/settings.json'), 'utf8')) as Record<string, any>
19
- const recommendations = (Bun.JSONC.parse(readFileSync(join(root, '.vscode/extensions.json'), 'utf8')) as { recommendations: string[] }).recommendations
20
-
21
- describe('marketplace identity', () => {
22
- it('is Stacks.vscode-stacks, not the stx extension', () => {
23
- const id = `${manifest.publisher}.${manifest.name}`
24
- expect(id).toBe('Stacks.vscode-stacks')
25
- expect(id.toLowerCase()).not.toBe(STX_EXTENSION_ID.toLowerCase())
26
- })
27
- })
28
-
29
- describe('self-contained', () => {
30
- it('installs no extension pack and depends on no extension', () => {
31
- expect(manifest.extensionPack).toBeUndefined()
32
- expect(manifest.extensionDependencies).toBeUndefined()
33
- })
34
-
35
- it('has no runtime dependencies to ship', () => {
36
- expect(manifest.dependencies).toBeUndefined()
37
- })
38
-
39
- it('recommends only itself to new projects', () => {
40
- expect(recommendations.map(id => id.toLowerCase())).toEqual(['stacks.vscode-stacks'])
41
- })
42
-
43
- it('configures new projects for no other extension', () => {
44
- // Settings that only mean something to an extension the project no longer
45
- // asks for: ESLint, Prettier, Biome, markdownlint, shell-format, cSpell,
46
- // Grammarly, vscode-icons, Todo Tree.
47
- const foreign = Object.keys(projectSettings).filter(key =>
48
- /^(?:eslint|prettier|biome|markdownlint|cSpell|grammarly|vsicons|todo-tree)\./.test(key))
49
- expect(foreign).toEqual([])
50
- expect(JSON.stringify(projectSettings)).not.toMatch(/dbaeumer|foxundermoon|DavidAnson|esbenp/i)
51
- })
52
-
53
- it('formats and fixes with pickier through this extension', () => {
54
- expect(projectSettings['editor.defaultFormatter']).toBe('Stacks.vscode-stacks')
55
- expect(projectSettings['editor.codeActionsOnSave']['source.fixAll.pickier']).toBe('explicit')
56
- })
57
- })
58
-
59
- describe('stx language support', () => {
60
- it('declares what @stacksjs/stx-vscode ships (run `bun run sync:stx` after updating it)', () => {
61
- expect(withStxContributes(manifest, libraryContributes(root))).toEqual(manifest)
62
- })
63
-
64
- it('declares no stx command the library no longer ships', () => {
65
- const shipped = new Set(libraryContributes(root).commands.map(command => command.command))
66
- const stale = manifest.contributes.commands
67
- .map((command: { command: string }) => command.command)
68
- .filter((id: string) => !id.startsWith(OWN_COMMAND_PREFIX) && !shipped.has(id))
69
- expect(stale).toEqual([])
70
- })
71
-
72
- it('drops a command stx renamed when syncing', () => {
73
- const stale = { ...manifest, contributes: { ...manifest.contributes, commands: [...manifest.contributes.commands, { command: 'css.reload', title: 'Css: Reload Configuration' }] } }
74
- expect(withStxContributes(stale, libraryContributes(root))).toEqual(manifest)
75
- })
76
-
77
- it('declares the stx language and its grammar from the bundled assets', () => {
78
- expect(manifest.contributes.languages.map((language: { id: string }) => language.id)).toContain('stx')
79
- expect(manifest.contributes.grammars[0].path).toStartWith('./dist/stx/')
80
- expect(manifest.files).toContain('dist/stx/**')
81
- expect(manifest.activationEvents).toContain('onLanguage:stx')
82
- })
83
-
84
- it('leaves snippets to runtime, so they never show twice next to the stx extension', () => {
85
- expect(manifest.contributes.snippets).toBeUndefined()
86
- })
87
-
88
- it('does not map .stx files to another language', () => {
89
- const extensionDefaults = manifest.contributes.configurationDefaults['files.associations'] ?? {}
90
- for (const associations of [extensionDefaults, projectSettings['files.associations'] ?? {}])
91
- expect(Object.keys(associations).filter(pattern => pattern.endsWith('.stx'))).toEqual([])
92
- })
93
- })
94
-
95
- // tsserver loads a plugin only by package name, from the extension's
96
- // node_modules, and VS Code only sends it documents of a language a plugin
97
- // claims (stacksjs/stx#2028).
98
- describe('stx type checking', () => {
99
- const plugins = manifest.contributes.typescriptServerPlugins as Array<{ name: string, languages?: string[], enableForWorkspaceTypeScriptVersions?: boolean }>
100
-
101
- it('contributes the stx TypeScript server plugin by package name', () => {
102
- expect(plugins.map(plugin => plugin.name)).toEqual(['@stacksjs/stx-typescript-plugin'])
103
- for (const plugin of plugins) {
104
- // The rule tsserver applies before loading anything (requestEnablePlugin).
105
- expect(plugin.name).not.toMatch(/^(?:\.\.?(?:\/|$)|\/|[a-z]:)/i)
106
- expect(plugin.name).not.toMatch(/[\\/]\.\.?(?:$|[\\/])/)
107
- }
108
- })
109
-
110
- it('claims the stx language, also with the workspace TypeScript the project settings select', () => {
111
- for (const plugin of plugins) {
112
- expect(plugin.languages).toEqual(['stx'])
113
- expect(plugin.enableForWorkspaceTypeScriptVersions).toBeTrue()
114
- }
115
- expect(manifest.contributes.configurationDefaults['typescript.tsdk']).toBe('node_modules/typescript/lib')
116
- })
117
-
118
- it('declares the setting that switches it off', () => {
119
- const settings = Object.assign({}, ...manifest.contributes.configuration.map((section: { properties: object }) => section.properties))
120
- expect(settings['stxTypescriptPlugin.enabled'].default).toBeTrue()
121
- })
122
- })
123
-
124
- describe('packaging', () => {
125
- it('adds the plugin to the VSIX, which vsce --no-dependencies leaves without node_modules', () => {
126
- expect(manifest.scripts.package).toBe('bun scripts/package.ts')
127
- expect(manifest.scripts.release).toBe('bun scripts/package.ts --publish')
128
- expect(readFileSync(join(root, 'scripts/package.ts'), 'utf8')).toContain(`'vsce', 'package', '--no-dependencies'`)
129
- // The release workflow publishes through the script, not a bare `vsce publish`.
130
- const workflow = readFileSync(join(root, '../../../../../.github/workflows/release.yml'), 'utf8')
131
- const step = workflow.slice(workflow.indexOf('- name: Publish VS Code Extension'))
132
- expect(step.slice(0, step.indexOf('- name:', 10))).toContain('bun run release')
133
- expect(workflow).not.toContain('vsce publish')
134
- })
135
-
136
- it('ships the files the extension loads at runtime', () => {
137
- for (const file of ['dist/extension.js', 'dist/pickier-worker.js', 'dist/pickier-worker.bunfig.toml'])
138
- expect(manifest.files).toContain(file)
139
- expect(manifest.main).toBe('./dist/extension.js')
140
- })
141
- })
@@ -1,47 +0,0 @@
1
- /**
2
- * The VSIX, as VS Code installs it. It is packaged with `--no-dependencies`,
3
- * which leaves node_modules out, so scripts/package.ts adds the stx TypeScript
4
- * plugin's entry there afterwards; without it tsserver finds no plugin and no
5
- * `.stx` file is type-checked.
6
- */
7
- import { describe, expect, it } from 'bun:test'
8
- import { mkdtempSync, readFileSync, realpathSync, rmSync } from 'node:fs'
9
- import { tmpdir } from 'node:os'
10
- import { join } from 'node:path'
11
- import { packageExtension } from '../scripts/package'
12
- import { TS_PLUGIN_DIST, tsPluginModuleDir, tsPluginName } from '../scripts/ts-plugin'
13
- import { expectDeclarationsFound, loadLikeTsserver } from './ts-plugin-harness'
14
-
15
- const root = join(import.meta.dir, '..')
16
- const manifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
17
-
18
- // vsce runs `vscode:prepublish` through npm, so this needs npm on PATH; the
19
- // release workflow, which runs these tests before publishing, has it.
20
- const canPackage = Boolean(Bun.which('npm') && Bun.which('zip') && Bun.which('unzip'))
21
-
22
- describe('the VSIX', () => {
23
- it.skipIf(!canPackage)('carries the stx TypeScript plugin, and it loads from the installed layout', () => {
24
- // Real path: require() resolves symlinks, and macOS's tmpdir is one.
25
- const dir = realpathSync(mkdtempSync(join(tmpdir(), 'stacks-vsix-test-')))
26
- try {
27
- const vsix = packageExtension(join(dir, 'stacks.vsix'))
28
- const listing = Bun.spawnSync(['unzip', '-Z1', vsix]).stdout.toString().split('\n')
29
- const name = tsPluginName(root)
30
-
31
- expect(listing).toContain(`extension/${tsPluginModuleDir(name)}/package.json`)
32
- expect(listing).toContain(`extension/${tsPluginModuleDir(name)}/index.js`)
33
- // Nothing else of node_modules: the bundle carries its dependencies.
34
- expect(listing.filter(file => file.startsWith('extension/node_modules/') && !file.startsWith(`extension/${tsPluginModuleDir(name)}/`))).toEqual([])
35
- expect(listing).toContain(`extension/${TS_PLUGIN_DIST}/types/stx-module.d.ts`)
36
-
37
- expect(Bun.spawnSync(['unzip', '-q', vsix, 'extension/*', '-d', dir]).exitCode).toBe(0)
38
- const installed = join(dir, 'extension')
39
- expect(JSON.parse(readFileSync(join(installed, 'package.json'), 'utf8')).version).toBe(manifest.version)
40
- expect(typeof loadLikeTsserver(installed, name)).toBe('function')
41
- expectDeclarationsFound(installed, name, join(installed, TS_PLUGIN_DIST, 'types'))
42
- }
43
- finally {
44
- rmSync(dir, { recursive: true, force: true })
45
- }
46
- }, 180_000)
47
- })
@@ -1,202 +0,0 @@
1
- /**
2
- * Pickier support (src/pickier.ts), driven through a fake `vscode` API and a
3
- * fake worker process: no editor, no Bun, no pickier.
4
- */
5
- import type { PickierProcess } from '../src/pickier'
6
- import type { PickierRequest, PickierResult } from '../src/pickier-protocol'
7
- import { describe, expect, it } from 'bun:test'
8
- import { chooseBun, createPickierSupport, disableNextLineComment, isLintedFile, isPickierConfigPath } from '../src/pickier'
9
- import { CodeActionKind, document, fakeVscode, flush } from './fake-vscode'
10
-
11
- const ROOT = '/work/app'
12
-
13
- interface Worker {
14
- answer?: (request: PickierRequest) => PickierResult
15
- ready?: false
16
- fixText?: boolean
17
- exitOnStart?: boolean
18
- }
19
-
20
- function setup(options: { worker?: Worker, files?: string[], settings?: Record<string, unknown>, documents?: ReturnType<typeof document>[] } = {}) {
21
- const files = new Set(options.files ?? [`${ROOT}/buddy`])
22
- const fake = fakeVscode({ folders: [ROOT], settings: options.settings, documents: options.documents })
23
- const spawned: Array<{ bun: string, script: string, cwd: string, requests: PickierRequest[] }> = []
24
-
25
- const host = {
26
- exists: (path: string) => files.has(path),
27
- spawnBun(bun: string, script: string, cwd: string): PickierProcess {
28
- const record = { bun, script, cwd, requests: [] as PickierRequest[] }
29
- spawned.push(record)
30
- const lineListeners: Array<(line: string) => void> = []
31
- const exitListeners: Array<() => void> = []
32
- const emit = (message: unknown) => queueMicrotask(() => lineListeners.forEach(listener => listener(JSON.stringify(message))))
33
- const worker = options.worker ?? {}
34
-
35
- queueMicrotask(() => {
36
- if (worker.exitOnStart)
37
- return exitListeners.forEach(listener => listener())
38
- if (worker.ready === false)
39
- return emit({ ready: false, error: 'Cannot find package \'pickier\'', missing: true })
40
- emit({ ready: true, version: '0.1.66', fixText: worker.fixText ?? true })
41
- })
42
-
43
- return {
44
- write(line) {
45
- const request = JSON.parse(line) as PickierRequest
46
- record.requests.push(request)
47
- const result = worker.answer?.(request) ?? (request.op === 'lint' ? { op: 'lint', issues: [] } : { op: request.op, text: null })
48
- emit({ id: request.id, ok: true, result })
49
- },
50
- onLine: listener => lineListeners.push(listener),
51
- onExit: listener => exitListeners.push(listener),
52
- kill: () => {},
53
- }
54
- },
55
- }
56
-
57
- const support = createPickierSupport(fake.api as any, host, { workerPath: '/ext/dist/pickier-worker.js', lintDelayMs: 1 })
58
- support.activate(fake.context)
59
- return { ...fake, spawned, support }
60
- }
61
-
62
- describe('helpers', () => {
63
- it('runs pickier on the configured Bun, else the project\'s pinned Bun, else bun on the PATH', () => {
64
- expect(chooseBun(' /opt/bun ', ROOT, () => true)).toBe('/opt/bun')
65
- expect(chooseBun('', ROOT, path => path === `${ROOT}/pantry/.bin/bun`)).toBe(`${ROOT}/pantry/.bin/bun`)
66
- expect(chooseBun('', ROOT, () => false)).toBe('bun')
67
- })
68
-
69
- it('lints exactly the files `buddy lint` does', () => {
70
- expect(isLintedFile(ROOT, `${ROOT}/app/Actions/Hello.ts`)).toBeTrue()
71
- expect(isLintedFile(ROOT, `${ROOT}/README.md`)).toBeTrue()
72
- expect(isLintedFile(ROOT, `${ROOT}/node_modules/x/index.ts`)).toBeFalse()
73
- expect(isLintedFile(ROOT, `${ROOT}/dist/index.js`)).toBeFalse()
74
- expect(isLintedFile(ROOT, `${ROOT}/resources/views/index.stx`)).toBeFalse()
75
- expect(isLintedFile(ROOT, '/elsewhere/a.ts')).toBeFalse()
76
- })
77
-
78
- it('recognises the files pickier reads its config from', () => {
79
- expect(isPickierConfigPath(`${ROOT}/config/code-style.ts`)).toBeTrue()
80
- expect(isPickierConfigPath(`${ROOT}/pickier.config.ts`)).toBeTrue()
81
- expect(isPickierConfigPath(`${ROOT}/.config/pickier.ts`)).toBeTrue()
82
- expect(isPickierConfigPath(`${ROOT}/config/app.ts`)).toBeFalse()
83
- })
84
-
85
- it('disables a rule for the next line at that line\'s indentation', () => {
86
- expect(disableNextLineComment(' let x = 1', 'prefer-const', '//')).toBe(' // pickier-disable-next-line prefer-const\n')
87
- })
88
- })
89
-
90
- describe('diagnostics', () => {
91
- it('reports the project pickier\'s issues on an open file, in editor coordinates', async () => {
92
- const file = document(`${ROOT}/app/x.ts`, 'import { a } from \'b\'\nlet x = 1\n')
93
- const { diagnostics, spawned } = setup({
94
- documents: [file],
95
- worker: {
96
- answer: () => ({ op: 'lint', issues: [{ line: 2, column: 5, ruleId: 'prefer-const', message: '\'x\' is never reassigned', severity: 'error', help: 'Use const' }] }),
97
- },
98
- })
99
- await flush()
100
-
101
- expect(spawned).toHaveLength(1)
102
- expect(spawned[0]).toMatchObject({ bun: 'bun', script: '/ext/dist/pickier-worker.js', cwd: ROOT })
103
- expect(spawned[0]!.requests[0]).toMatchObject({ op: 'lint', file: `${ROOT}/app/x.ts` })
104
-
105
- const [diagnostic] = diagnostics.get(`file://${ROOT}/app/x.ts`)!
106
- expect(diagnostic.source).toBe('pickier')
107
- expect(diagnostic.code).toBe('prefer-const')
108
- expect(diagnostic.severity).toBe(0)
109
- expect(diagnostic.range.start).toMatchObject({ line: 1, character: 4 })
110
- expect(diagnostic.range.end).toMatchObject({ line: 1, character: 5 })
111
- expect(diagnostic.message).toBe('\'x\' is never reassigned\nUse const')
112
- })
113
-
114
- it('leaves folders that are not Stacks projects alone', async () => {
115
- const { spawned } = setup({ files: [], documents: [document(`${ROOT}/x.ts`, 'let x = 1\n')] })
116
- await flush()
117
- expect(spawned).toHaveLength(0)
118
- })
119
-
120
- it('lints nothing when turned off', async () => {
121
- const { spawned } = setup({ settings: { 'pickier.enable': false }, documents: [document(`${ROOT}/x.ts`, 'let x = 1\n')] })
122
- await flush()
123
- expect(spawned).toHaveLength(0)
124
- })
125
-
126
- it('stops asking a project with no pickier, and says why once', async () => {
127
- const { spawned, events, output, diagnostics } = setup({ worker: { ready: false }, documents: [document(`${ROOT}/a.ts`, '')] })
128
- await flush()
129
- events.opened.fire(document(`${ROOT}/b.ts`, '') as any)
130
- await flush()
131
-
132
- expect(spawned).toHaveLength(1)
133
- expect(diagnostics.size).toBe(0)
134
- expect(output.filter(line => line.includes('not available'))).toHaveLength(1)
135
- })
136
-
137
- it('gives up on a worker that keeps exiting instead of respawning it on every keystroke', async () => {
138
- const { spawned, events } = setup({ worker: { exitOnStart: true } })
139
- for (let i = 0; i < 5; i++) {
140
- events.opened.fire(document(`${ROOT}/x${i}.ts`, '') as any)
141
- await flush()
142
- }
143
- expect(spawned).toHaveLength(3)
144
- })
145
- })
146
-
147
- describe('fixing and formatting', () => {
148
- const FIXED = 'const x = 1\n'
149
-
150
- it('offers source.fixAll.pickier, the code action codeActionsOnSave asks for', async () => {
151
- const file = document(`${ROOT}/x.ts`, 'let x = 1\n')
152
- const { codeActionProviders, spawned } = setup({ worker: { answer: request => request.op === 'fix' ? { op: 'fix', text: FIXED } : { op: 'lint', issues: [] } } })
153
-
154
- const only = CodeActionKind.SourceFixAll.append('pickier')
155
- const [action] = await codeActionProviders[0].provideCodeActions(file, undefined, { only, diagnostics: [] })
156
-
157
- expect(action.kind.value).toBe('source.fixAll.pickier')
158
- expect(action.edit.edits[0].text).toBe(FIXED)
159
- expect(spawned[0]!.requests.at(-1)!.op).toBe('fix')
160
-
161
- // The generic `source.fixAll` includes it too.
162
- const generic = await codeActionProviders[0].provideCodeActions(file, undefined, { only: CodeActionKind.SourceFixAll, diagnostics: [] })
163
- expect(generic).toHaveLength(1)
164
- })
165
-
166
- it('offers nothing when there is nothing to fix, or the project\'s pickier cannot fix text', async () => {
167
- const file = document(`${ROOT}/x.ts`, FIXED)
168
- const unchanged = setup({ worker: { answer: request => ({ op: request.op as 'fix', text: FIXED }) } })
169
- expect(await unchanged.codeActionProviders[0].provideCodeActions(file, undefined, { only: CodeActionKind.SourceFixAll, diagnostics: [] })).toEqual([])
170
-
171
- const old = setup({ worker: { fixText: false } })
172
- expect(await old.codeActionProviders[0].provideCodeActions(document(`${ROOT}/x.ts`, 'let x = 1\n'), undefined, { only: CodeActionKind.SourceFixAll, diagnostics: [] })).toEqual([])
173
- })
174
-
175
- it('offers quick fixes for its own diagnostics: fix all, and disable the rule for the line', async () => {
176
- const file = document(`${ROOT}/x.ts`, 'function f() {\n let x = 1\n}\n')
177
- const { codeActionProviders } = setup({ worker: { answer: request => request.op === 'fix' ? { op: 'fix', text: 'function f() {\n const x = 1\n}\n' } : { op: 'lint', issues: [] } } })
178
- const diagnostic = { source: 'pickier', code: 'prefer-const', range: { start: { line: 1, character: 6 } } }
179
-
180
- const actions = await codeActionProviders[0].provideCodeActions(file, undefined, { diagnostics: [diagnostic, { source: 'ts', code: 1 }] })
181
-
182
- expect(actions.map((action: any) => action.title)).toEqual(['Fix all pickier problems', 'Disable prefer-const for this line'])
183
- expect(actions[1].edit.edits[0]).toMatchObject({ position: { line: 1, character: 0 }, text: ' // pickier-disable-next-line prefer-const\n' })
184
- })
185
-
186
- it('formats with the project\'s pickier', async () => {
187
- const file = document(`${ROOT}/x.ts`, 'const a = "x"\n')
188
- const { formatters } = setup({ worker: { answer: request => request.op === 'format' ? { op: 'format', text: 'const a = \'x\'\n' } : { op: 'lint', issues: [] } } })
189
-
190
- const [edit] = await formatters[0].provideDocumentFormattingEdits(file)
191
- expect(edit.newText).toBe('const a = \'x\'\n')
192
- })
193
-
194
- it('applies the fixes to the active file from the command palette', async () => {
195
- const file = document(`${ROOT}/x.ts`, 'let x = 1\n')
196
- const fake = setup({ worker: { answer: request => request.op === 'fix' ? { op: 'fix', text: FIXED } : { op: 'lint', issues: [] } } })
197
- fake.api.window.activeTextEditor = { document: file }
198
-
199
- await fake.handlers.get('stacks.pickier.fixAll')!()
200
- expect(fake.applied[0]!.edits[0]!.text).toBe(FIXED)
201
- })
202
- })