diff --git a/packages/twoslash-vue/src/index.ts b/packages/twoslash-vue/src/index.ts index 28a55de..b3c2453 100644 --- a/packages/twoslash-vue/src/index.ts +++ b/packages/twoslash-vue/src/index.ts @@ -1,8 +1,11 @@ import type { VueCompilerOptions } from '@vue/language-core' import { SourceMap, createVueLanguage, sharedTypes } from '@vue/language-core' +import type { CompilerOptions } from 'typescript' import ts from 'typescript' import type { CreateTwoslashOptions, + HandbookOptions, + ParsedFlagNotation, Range, TwoslashExecuteOptions, TwoslashInstance, @@ -12,10 +15,14 @@ import { createPositionConverter, createTwoslasher as createTwoslasherBase, defaultCompilerOptions, + defaultHandbookOptions, + findFlagNotations, findQueryMarkers, + objectHash, removeCodeRanges, resolveNodePositions, } from 'twoslash' +import type { CompilerOptionDeclaration } from '../../twoslash/src/types/internal' export interface VueSpecificOptions { /** @@ -41,34 +48,73 @@ export interface TwoslashVueExecuteOptions extends TwoslashExecuteOptions, VueSp */ export function createTwoslasher(createOptions: CreateTwoslashVueOptions = {}): TwoslashInstance { const twoslasherBase = createTwoslasherBase(createOptions) + const cache = twoslasherBase.getCacheMap() as any as Map> | undefined + const tsOptionDeclarations = (ts as any).optionDeclarations as CompilerOptionDeclaration[] + + function getVueLanguage(compilerOptions: Partial, vueCompilerOptions: Partial) { + if (!cache) + return createVueLanguage(ts, defaultCompilerOptions, vueCompilerOptions) + const key = `vue:${objectHash([compilerOptions, vueCompilerOptions])}` + if (!cache.has(key)) { + const env = createVueLanguage(ts, defaultCompilerOptions, vueCompilerOptions) + cache.set(key, env) + return env + } + return cache.get(key)! + } function twoslasher(code: string, extension?: string, options: TwoslashVueExecuteOptions = {}) { if (extension !== 'vue') return twoslasherBase(code, extension, options) - // TODO: use cache like twoslasherBase - const lang = createVueLanguage( - ts, - { - ...defaultCompilerOptions, - ...options.compilerOptions, - }, - { - ...createOptions.vueCompilerOptions, - ...options.vueCompilerOptions, - }, - ) + const vueCompilerOptions: Partial = { + ...createOptions.vueCompilerOptions, + ...options.vueCompilerOptions, + } + const compilerOptions: Partial = { + ...defaultCompilerOptions, + ...options.compilerOptions, + } + const handbookOptions: Partial = { + ...defaultHandbookOptions, + noErrorsCutted: true, + ...options.handbookOptions, + } const sourceMeta = { removals: [] as Range[], positionCompletions: [] as number[], positionQueries: [] as number[], positionHighlights: [] as Range[], + flagNotations: [] as ParsedFlagNotation[], } satisfies Partial + const { + customTags = createOptions.customTags || [], + } = options + const pc = createPositionConverter(code) // we get the markers with the original code so the position is correct findQueryMarkers(code, sourceMeta, pc.getIndexOfLineAbove) + const flagNotations = findFlagNotations(code, customTags, tsOptionDeclarations) + + // #region apply flags + for (const flag of flagNotations) { + switch (flag.type) { + case 'unknown': + continue + + case 'compilerOptions': + compilerOptions[flag.name] = flag.value + break + case 'handbookOptions': + // @ts-expect-error -- this is fine + handbookOptions[flag.name] = flag.value + break + } + sourceMeta.removals.push([flag.start, flag.end]) + } + // #endregion // replace non-whitespace in the already extracted markers let strippedCode = code @@ -79,6 +125,7 @@ export function createTwoslasher(createOptions: CreateTwoslashVueOptions = {}): + strippedCode.slice(end) } + const lang = getVueLanguage(compilerOptions, vueCompilerOptions) const fileSource = lang.createVirtualFile('index.vue', ts.ScriptSnapshot.fromString(strippedCode), 'vue')! const fileCompiled = fileSource.getEmbeddedFiles()[0] const typeHelpers = sharedTypes.getTypesCode(fileSource.vueCompilerOptions) @@ -90,6 +137,13 @@ export function createTwoslasher(createOptions: CreateTwoslashVueOptions = {}): const map = new SourceMap(fileCompiled.mappings) + function getLastGeneratedOffset(pos: number) { + const offsets = [...map.toGeneratedOffsets(pos)] + if (!offsets.length) + return undefined + return offsets[offsets.length - 1]?.[0] + } + // Pass compiled to TS file to twoslash const result = twoslasherBase(compiled, 'tsx', { ...options, @@ -97,11 +151,10 @@ export function createTwoslasher(createOptions: CreateTwoslashVueOptions = {}): jsx: 4 satisfies ts.JsxEmit.ReactJSX, jsxImportSource: 'vue', noImplicitAny: false, - ...options.compilerOptions, + ...compilerOptions, }, handbookOptions: { - noErrorsCutted: true, - ...options.handbookOptions, + ...handbookOptions, keepNotations: true, }, shouldGetHoverInfo(id) { @@ -109,7 +162,7 @@ export function createTwoslasher(createOptions: CreateTwoslashVueOptions = {}): return !id.startsWith('__VLS') }, positionCompletions: sourceMeta.positionCompletions - .map(p => map.toGeneratedOffset(p)![0]), + .map(p => getLastGeneratedOffset(p)!), positionQueries: sourceMeta.positionQueries .map(p => map.toGeneratedOffset(p)![0]), positionHighlights: sourceMeta.positionHighlights @@ -124,13 +177,19 @@ export function createTwoslasher(createOptions: CreateTwoslashVueOptions = {}): .map((q) => { if ('text' in q && q.text === 'any') return undefined - const start = map.toSourceOffset(q.start)?.[0] - const end = map.toSourceOffset(q.start + q.length)?.[0] - if (start == null || end == null || start < 0 || end < 0 || start >= end) + const startMap = map.toSourceOffset(q.start) + if (!startMap) + return undefined + const start = startMap[0] + let end = map.toSourceOffset(q.start + q.length)?.[0] + if (end == null && startMap[1].sourceRange[0] === startMap[0]) + end = startMap[1].sourceRange[1] + if (end == null || start < 0 || end < 0 || start > end) return undefined return Object.assign(q, { ...q, - start, + target: code.slice(start, end), + start: startMap[0], length: end - start, }) }) @@ -159,6 +218,16 @@ export function createTwoslasher(createOptions: CreateTwoslashVueOptions = {}): result.meta.removals = mappedRemovals } + result.nodes = result.nodes.filter((n, idx) => { + const next = result.nodes[idx + 1] + if (!next) + return true + // When multiple nodes are on the same position, we keep the last one by ignoring the previous ones + if (next.type === n.type && next.start === n.start) + return false + return true + }) + result.meta.extension = 'vue' return result diff --git a/packages/twoslash-vue/test/fixtures.test.ts b/packages/twoslash-vue/test/fixtures.test.ts new file mode 100644 index 0000000..ba9b53f --- /dev/null +++ b/packages/twoslash-vue/test/fixtures.test.ts @@ -0,0 +1,73 @@ +/// + +import { extname } from 'node:path' +import process from 'node:process' +import { expect, it } from 'vitest' +import type { TwoslashReturn } from 'twoslash' +import { createTwoslasher } from '../src/index' + +// To add a test, create a file in the fixtures folder and it will will run through +// as though it was the codeblock. + +const fixtures = import.meta.glob('./fixtures/**/*.*', { as: 'raw' }) + +// A temporary list of regex to match with the path of the file to test +const filters: RegExp[] = [ + // /completions-files/, +] + +if (process.env.CI && filters.length) + throw new Error('Should not filters fixture tests in CI, did you forget to remove them?') + +const twoslasher = createTwoslasher() + +Object.entries(fixtures).forEach(([path, fixture]) => { + path = path.replace(/\\/g, '/') + const expectThrows = path.includes('/throws/') + const inExt = extname(path).slice(1) + const outExt = expectThrows ? '.txt' : '.json' + const outPath = path.replace('/fixtures/', '/results/').replace(/\.[^/.]+$/, outExt) + + it.skipIf(filters.length && !filters.some(f => path.match(f)))( + path, + async () => { + let result: TwoslashReturn = undefined! + try { + result = twoslasher( + await fixture(), + inExt, + { + customTags: ['annotate'], + }, + ) + } + catch (err: any) { + if (expectThrows) { + expect(err.message).toMatchFileSnapshot(outPath) + return + } + else { + throw err + } + } + + if (expectThrows) { + throw new Error('Expected to throw') + } + + else { + expect(cleanFixture(result)) + .toMatchFileSnapshot(outPath) + } + }, + ) +}) + +function cleanFixture(result: TwoslashReturn) { + return JSON.stringify({ + code: result.code, + nodes: result.nodes, + flags: result.meta.flagNotations, + // compilerOptions: ts.meta.compilerOptions + }, null, 2).replaceAll(process.cwd(), '[home]') +} diff --git a/packages/twoslash-vue/test/fixtures/completion.vue b/packages/twoslash-vue/test/fixtures/completion.vue new file mode 100644 index 0000000..e19811d --- /dev/null +++ b/packages/twoslash-vue/test/fixtures/completion.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/twoslash-vue/test/fixtures/example.vue b/packages/twoslash-vue/test/fixtures/example.vue new file mode 100644 index 0000000..125c6d9 --- /dev/null +++ b/packages/twoslash-vue/test/fixtures/example.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/packages/twoslash-vue/test/fixtures/query-basic.vue b/packages/twoslash-vue/test/fixtures/query-basic.vue new file mode 100644 index 0000000..b3230e7 --- /dev/null +++ b/packages/twoslash-vue/test/fixtures/query-basic.vue @@ -0,0 +1,15 @@ + + + diff --git a/packages/twoslash-vue/test/query.test.ts b/packages/twoslash-vue/test/query.test.ts index 7dce501..53234c3 100644 --- a/packages/twoslash-vue/test/query.test.ts +++ b/packages/twoslash-vue/test/query.test.ts @@ -1,21 +1,7 @@ import { describe, expect, it } from 'vitest' import { createTwoslasher } from '../src/index' -const code = ` - - - -` +const code = await import('./fixtures/query-basic.vue?raw').then(m => m.default) const twoslasher = createTwoslasher() @@ -26,7 +12,7 @@ describe('basic', () => { expect(result.nodes.find(n => n.type === 'hover' && n.target === 'button')) .toHaveProperty('text', '(property) button: ButtonHTMLAttributes & ReservedProps') expect(result.nodes.find(n => n.type === 'hover' && n.target === 'click')) - .toHaveProperty('text', '(property) \'click\': ((payload: MouseEvent) => void) | undefined') + .toHaveProperty('text', `(property) 'click': ((payload: MouseEvent) => void) | undefined`) }) it('has correct query', () => { @@ -35,7 +21,8 @@ describe('basic', () => { [ 38, 235, - 1970, + 2023, + 2624, ] `) @@ -48,8 +35,8 @@ describe('basic', () => { "character": 14, "docs": undefined, "length": 8, - "line": 2, - "start": 40, + "line": 1, + "start": 39, "tags": undefined, "target": "computed", "text": "(alias) const computed: { @@ -61,10 +48,36 @@ describe('basic', () => { } `) - // TODO: support this, and also it should throw an error if it's not found - // expect(result.nodes.find(n => n.type === 'query' && n.target === 'click')) - // .toMatchInlineSnapshot(`undefined`) - // expect(result.nodes.filter(n => n.type === 'query')) - // .toHaveLength(3) + expect(result.nodes.find(n => n.type === 'query' && n.target === 'count')) + .toMatchInlineSnapshot(` + { + "character": 18, + "docs": undefined, + "length": 5, + "line": 9, + "start": 228, + "tags": undefined, + "target": "count", + "text": "(property) count: number", + "type": "query", + } + `) + + expect(result.nodes.find(n => n.type === 'query' && n.target === 'click')) + .toMatchInlineSnapshot(` + { + "character": 11, + "docs": undefined, + "length": 5, + "line": 8, + "start": 163, + "tags": undefined, + "target": "click", + "text": "(property) onClick?: ((payload: MouseEvent) => void) | undefined", + "type": "query", + } + `) + expect(result.nodes.filter(n => n.type === 'query')) + .toHaveLength(4) }) }) diff --git a/packages/twoslash-vue/test/results/completion.json b/packages/twoslash-vue/test/results/completion.json new file mode 100644 index 0000000..c6b0302 --- /dev/null +++ b/packages/twoslash-vue/test/results/completion.json @@ -0,0 +1,252 @@ +{ + "code": "\n\n\n", + "nodes": [ + { + "type": "hover", + "text": "(alias) function ref(value: T): Ref> (+1 overload)\nimport ref", + "docs": "Takes an inner value and returns a reactive and mutable ref object, which\nhas a single property `.value` that points to the inner value.", + "tags": [ + [ + "param", + "value - The object to wrap in the ref." + ], + [ + "see", + "{@link https://vuejs.org/api/reactivity-core.html#ref}" + ] + ], + "start": 34, + "length": 3, + "target": "ref", + "line": 1, + "character": 9 + }, + { + "type": "hover", + "text": "const count1: Ref", + "start": 58, + "length": 6, + "target": "count1", + "line": 3, + "character": 6 + }, + { + "type": "hover", + "text": "(alias) ref(value: number): Ref (+1 overload)\nimport ref", + "docs": "Takes an inner value and returns a reactive and mutable ref object, which\nhas a single property `.value` that points to the inner value.", + "tags": [ + [ + "param", + "value - The object to wrap in the ref." + ], + [ + "see", + "{@link https://vuejs.org/api/reactivity-core.html#ref}" + ] + ], + "start": 67, + "length": 3, + "target": "ref", + "line": 3, + "character": 15 + }, + { + "type": "hover", + "text": "function count2(): number", + "start": 84, + "length": 6, + "target": "count2", + "line": 5, + "character": 9 + }, + { + "type": "hover", + "text": "const count1: Ref", + "start": 104, + "length": 6, + "target": "count1", + "line": 6, + "character": 9 + }, + { + "type": "hover", + "text": "(property) Ref.value: number", + "start": 111, + "length": 5, + "target": "value", + "line": 6, + "character": 16 + }, + { + "type": "hover", + "text": "(property) div: HTMLAttributes & ReservedProps", + "start": 148, + "length": 3, + "target": "div", + "line": 11, + "character": 3 + }, + { + "type": "hover", + "text": "const count1: Ref", + "start": 160, + "length": 6, + "target": "count1", + "line": 12, + "character": 7 + }, + { + "type": "completion", + "start": 161, + "length": 0, + "completions": [ + { + "name": "count1", + "kind": "const", + "kindModifiers": "", + "sortText": "11" + }, + { + "name": "count2", + "kind": "function", + "kindModifiers": "", + "sortText": "11" + }, + { + "name": "caches", + "kind": "var", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "cancelAnimationFrame", + "kind": "function", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "cancelIdleCallback", + "kind": "function", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "case", + "kind": "keyword", + "kindModifiers": "", + "sortText": "15" + }, + { + "name": "catch", + "kind": "keyword", + "kindModifiers": "", + "sortText": "15" + }, + { + "name": "class", + "kind": "keyword", + "kindModifiers": "", + "sortText": "15" + }, + { + "name": "clearInterval", + "kind": "function", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "clearTimeout", + "kind": "function", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "close", + "kind": "function", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "closed", + "kind": "var", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "confirm", + "kind": "function", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "console", + "kind": "var", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "const", + "kind": "keyword", + "kindModifiers": "", + "sortText": "15" + }, + { + "name": "continue", + "kind": "keyword", + "kindModifiers": "", + "sortText": "15" + }, + { + "name": "createImageBitmap", + "kind": "function", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "crossOriginIsolated", + "kind": "var", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "crypto", + "kind": "var", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "customElements", + "kind": "var", + "kindModifiers": "declare", + "sortText": "15" + }, + { + "name": "captureEvents", + "kind": "function", + "kindModifiers": "deprecated,declare", + "sortText": "z15" + }, + { + "name": "clientInformation", + "kind": "var", + "kindModifiers": "deprecated,declare", + "sortText": "z15" + } + ], + "completionsPrefix": "c", + "line": 12, + "character": 8, + "target": "" + }, + { + "type": "hover", + "text": "(property) div: HTMLAttributes & ReservedProps", + "start": 174, + "length": 3, + "target": "div", + "line": 13, + "character": 4 + } + ], + "flags": [] +} \ No newline at end of file diff --git a/packages/twoslash-vue/test/results/example.json b/packages/twoslash-vue/test/results/example.json new file mode 100644 index 0000000..ff539f3 --- /dev/null +++ b/packages/twoslash-vue/test/results/example.json @@ -0,0 +1,273 @@ +{ + "code": "\n\n\n\n\n", + "nodes": [ + { + "type": "hover", + "text": "(alias) function ref(value: T): Ref> (+1 overload)\nimport ref", + "docs": "Takes an inner value and returns a reactive and mutable ref object, which\nhas a single property `.value` that points to the inner value.", + "tags": [ + [ + "param", + "value - The object to wrap in the ref." + ], + [ + "see", + "{@link https://vuejs.org/api/reactivity-core.html#ref}" + ] + ], + "start": 34, + "length": 3, + "target": "ref", + "line": 1, + "character": 9 + }, + { + "type": "hover", + "text": "(alias) const computed: {\n (getter: ComputedGetter, debugOptions?: DebuggerOptions | undefined): ComputedRef;\n (options: WritableComputedOptions, debugOptions?: DebuggerOptions | undefined): WritableComputedRef<...>;\n}\nimport computed", + "start": 39, + "length": 8, + "target": "computed", + "line": 1, + "character": 14 + }, + { + "type": "query", + "text": "(alias) const computed: {\n (getter: ComputedGetter, debugOptions?: DebuggerOptions | undefined): ComputedRef;\n (options: WritableComputedOptions, debugOptions?: DebuggerOptions | undefined): WritableComputedRef<...>;\n}\nimport computed", + "start": 39, + "length": 8, + "target": "computed", + "line": 1, + "character": 14 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 72, + "length": 5, + "target": "count", + "line": 7, + "character": 6 + }, + { + "type": "hover", + "text": "(alias) ref(value: number): Ref (+1 overload)\nimport ref", + "docs": "Takes an inner value and returns a reactive and mutable ref object, which\nhas a single property `.value` that points to the inner value.", + "tags": [ + [ + "param", + "value - The object to wrap in the ref." + ], + [ + "see", + "{@link https://vuejs.org/api/reactivity-core.html#ref}" + ] + ], + "start": 80, + "length": 3, + "target": "ref", + "line": 7, + "character": 14 + }, + { + "type": "hover", + "text": "const double: ComputedRef", + "start": 94, + "length": 6, + "target": "double", + "line": 9, + "character": 6 + }, + { + "type": "query", + "text": "const double: ComputedRef", + "start": 94, + "length": 6, + "target": "double", + "line": 9, + "character": 6 + }, + { + "type": "hover", + "text": "(alias) computed(getter: ComputedGetter, debugOptions?: DebuggerOptions | undefined): ComputedRef (+1 overload)\nimport computed", + "docs": "Takes a getter function and returns a readonly reactive ref object for the\nreturned value from the getter. It can also take an object with get and set\nfunctions to create a writable ref object.", + "tags": [ + [ + "example", + "```js\n// Creating a readonly computed ref:\nconst count = ref(1)\nconst plusOne = computed(() => count.value + 1)\n\nconsole.log(plusOne.value) // 2\nplusOne.value++ // error\n```\n\n```js\n// Creating a writable computed ref:\nconst count = ref(1)\nconst plusOne = computed({\n get: () => count.value + 1,\n set: (val) => {\n count.value = val - 1\n }\n})\n\nplusOne.value = 1\nconsole.log(count.value) // 0\n```" + ], + [ + "param", + "getter - Function that produces the next value." + ], + [ + "param", + "debugOptions - For debugging. See {@link https://vuejs.org/guide/extras/reactivity-in-depth.html#computed-debugging}." + ], + [ + "see", + "{@link https://vuejs.org/api/reactivity-core.html#computed}" + ] + ], + "start": 103, + "length": 8, + "target": "computed", + "line": 9, + "character": 15 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 118, + "length": 5, + "target": "count", + "line": 9, + "character": 30 + }, + { + "type": "hover", + "text": "(property) Ref.value: number", + "start": 124, + "length": 5, + "target": "value", + "line": 9, + "character": 36 + }, + { + "type": "hover", + "text": "(property) ComponentOptionsBase = {}>.name?: string | undefined", + "start": 174, + "length": 4, + "target": "name", + "line": 14, + "character": 2 + }, + { + "type": "hover", + "text": "(property) LegacyOptions<{}, { msg: string; }, {}, { greet(): void; }, ComponentOptionsMixin, ComponentOptionsMixin, {}, string>.data?: ((this: CreateComponentPublicInstance<...>, vm: CreateComponentPublicInstance<{}, {}, {}, {}, MethodOptions, ComponentOptionsMixin, ComponentOptionsMixin, {}, {}, {}, false, ... 8 more ..., {}>) => {\n ...;\n}) | undefined", + "start": 196, + "length": 4, + "target": "data", + "line": 15, + "character": 2 + }, + { + "type": "hover", + "text": "(property) msg: string", + "start": 224, + "length": 3, + "target": "msg", + "line": 17, + "character": 6 + }, + { + "type": "hover", + "text": "(property) LegacyOptions<{}, { msg: string; }, {}, { greet(): void; }, ComponentOptionsMixin, ComponentOptionsMixin, {}, string>.methods?: {\n greet(): void;\n} | undefined", + "start": 251, + "length": 7, + "target": "methods", + "line": 20, + "character": 2 + }, + { + "type": "hover", + "text": "(method) greet(): void", + "start": 266, + "length": 5, + "target": "greet", + "line": 21, + "character": 4 + }, + { + "type": "hover", + "text": "var console: Console", + "start": 282, + "length": 7, + "target": "console", + "line": 22, + "character": 6 + }, + { + "type": "hover", + "text": "(method) Console.log(...data: any[]): void", + "docs": "[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log)", + "start": 290, + "length": 3, + "target": "log", + "line": 22, + "character": 14 + }, + { + "type": "hover", + "text": "(property) msg: string", + "start": 299, + "length": 3, + "target": "msg", + "line": 22, + "character": 23 + }, + { + "type": "hover", + "text": "(property) button: ButtonHTMLAttributes & ReservedProps", + "start": 341, + "length": 6, + "target": "button", + "line": 29, + "character": 3 + }, + { + "type": "hover", + "text": "(property) 'click': ((payload: MouseEvent) => void) | undefined", + "start": 349, + "length": 5, + "target": "click", + "line": 29, + "character": 11 + }, + { + "type": "query", + "text": "(property) onClick?: ((payload: MouseEvent) => void) | undefined", + "start": 349, + "length": 5, + "target": "click", + "line": 29, + "character": 11 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 356, + "length": 5, + "target": "count", + "line": 29, + "character": 18 + }, + { + "type": "hover", + "text": "(property) msg: string", + "start": 368, + "length": 3, + "target": "msg", + "line": 29, + "character": 30 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 388, + "length": 5, + "target": "count", + "line": 29, + "character": 50 + }, + { + "type": "hover", + "text": "(property) button: ButtonHTMLAttributes & ReservedProps", + "start": 398, + "length": 6, + "target": "button", + "line": 29, + "character": 60 + } + ], + "flags": [] +} \ No newline at end of file diff --git a/packages/twoslash-vue/test/results/query-basic.json b/packages/twoslash-vue/test/results/query-basic.json new file mode 100644 index 0000000..6907a31 --- /dev/null +++ b/packages/twoslash-vue/test/results/query-basic.json @@ -0,0 +1,227 @@ +{ + "code": "\n\n\n", + "nodes": [ + { + "type": "hover", + "text": "(alias) function ref(value: T): Ref> (+1 overload)\nimport ref", + "docs": "Takes an inner value and returns a reactive and mutable ref object, which\nhas a single property `.value` that points to the inner value.", + "tags": [ + [ + "param", + "value - The object to wrap in the ref." + ], + [ + "see", + "{@link https://vuejs.org/api/reactivity-core.html#ref}" + ] + ], + "start": 34, + "length": 3, + "target": "ref", + "line": 1, + "character": 9 + }, + { + "type": "hover", + "text": "(alias) const computed: {\n (getter: ComputedGetter, debugOptions?: DebuggerOptions | undefined): ComputedRef;\n (options: WritableComputedOptions, debugOptions?: DebuggerOptions | undefined): WritableComputedRef<...>;\n}\nimport computed", + "start": 39, + "length": 8, + "target": "computed", + "line": 1, + "character": 14 + }, + { + "type": "query", + "text": "(alias) const computed: {\n (getter: ComputedGetter, debugOptions?: DebuggerOptions | undefined): ComputedRef;\n (options: WritableComputedOptions, debugOptions?: DebuggerOptions | undefined): WritableComputedRef<...>;\n}\nimport computed", + "start": 39, + "length": 8, + "target": "computed", + "line": 1, + "character": 14 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 68, + "length": 5, + "target": "count", + "line": 3, + "character": 6 + }, + { + "type": "hover", + "text": "(alias) ref(value: number): Ref (+1 overload)\nimport ref", + "docs": "Takes an inner value and returns a reactive and mutable ref object, which\nhas a single property `.value` that points to the inner value.", + "tags": [ + [ + "param", + "value - The object to wrap in the ref." + ], + [ + "see", + "{@link https://vuejs.org/api/reactivity-core.html#ref}" + ] + ], + "start": 76, + "length": 3, + "target": "ref", + "line": 3, + "character": 14 + }, + { + "type": "hover", + "text": "const double: ComputedRef", + "start": 89, + "length": 6, + "target": "double", + "line": 4, + "character": 6 + }, + { + "type": "query", + "text": "const double: ComputedRef", + "start": 89, + "length": 6, + "target": "double", + "line": 4, + "character": 6 + }, + { + "type": "hover", + "text": "(alias) computed(getter: ComputedGetter, debugOptions?: DebuggerOptions | undefined): ComputedRef (+1 overload)\nimport computed", + "docs": "Takes a getter function and returns a readonly reactive ref object for the\nreturned value from the getter. It can also take an object with get and set\nfunctions to create a writable ref object.", + "tags": [ + [ + "example", + "```js\n// Creating a readonly computed ref:\nconst count = ref(1)\nconst plusOne = computed(() => count.value + 1)\n\nconsole.log(plusOne.value) // 2\nplusOne.value++ // error\n```\n\n```js\n// Creating a writable computed ref:\nconst count = ref(1)\nconst plusOne = computed({\n get: () => count.value + 1,\n set: (val) => {\n count.value = val - 1\n }\n})\n\nplusOne.value = 1\nconsole.log(count.value) // 0\n```" + ], + [ + "param", + "getter - Function that produces the next value." + ], + [ + "param", + "debugOptions - For debugging. See {@link https://vuejs.org/guide/extras/reactivity-in-depth.html#computed-debugging}." + ], + [ + "see", + "{@link https://vuejs.org/api/reactivity-core.html#computed}" + ] + ], + "start": 98, + "length": 8, + "target": "computed", + "line": 4, + "character": 15 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 113, + "length": 5, + "target": "count", + "line": 4, + "character": 30 + }, + { + "type": "hover", + "text": "(property) Ref.value: number", + "start": 119, + "length": 5, + "target": "value", + "line": 4, + "character": 36 + }, + { + "type": "hover", + "text": "(property) button: ButtonHTMLAttributes & ReservedProps", + "start": 155, + "length": 6, + "target": "button", + "line": 8, + "character": 3 + }, + { + "type": "hover", + "text": "(property) 'click': ((payload: MouseEvent) => void) | undefined", + "start": 163, + "length": 5, + "target": "click", + "line": 8, + "character": 11 + }, + { + "type": "query", + "text": "(property) onClick?: ((payload: MouseEvent) => void) | undefined", + "start": 163, + "length": 5, + "target": "click", + "line": 8, + "character": 11 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 170, + "length": 5, + "target": "count", + "line": 8, + "character": 18 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 192, + "length": 5, + "target": "count", + "line": 8, + "character": 40 + }, + { + "type": "hover", + "text": "(property) button: ButtonHTMLAttributes & ReservedProps", + "start": 202, + "length": 6, + "target": "button", + "line": 8, + "character": 50 + }, + { + "type": "hover", + "text": "(property) p: HTMLAttributes & ReservedProps", + "start": 213, + "length": 1, + "target": "p", + "line": 9, + "character": 3 + }, + { + "type": "hover", + "text": "const count: Ref", + "start": 228, + "length": 5, + "target": "count", + "line": 9, + "character": 18 + }, + { + "type": "query", + "text": "(property) count: number", + "start": 228, + "length": 5, + "target": "count", + "line": 9, + "character": 18 + }, + { + "type": "hover", + "text": "(property) p: HTMLAttributes & ReservedProps", + "start": 238, + "length": 1, + "target": "p", + "line": 9, + "character": 28 + } + ], + "flags": [] +} \ No newline at end of file diff --git a/packages/twoslash-vue/test/results/example.raw.html b/packages/twoslash-vue/test/results/renderer/example.raw.html similarity index 100% rename from packages/twoslash-vue/test/results/example.raw.html rename to packages/twoslash-vue/test/results/renderer/example.raw.html diff --git a/packages/twoslash-vue/test/results/example.vue.html b/packages/twoslash-vue/test/results/renderer/example.vue.html similarity index 87% rename from packages/twoslash-vue/test/results/example.vue.html rename to packages/twoslash-vue/test/results/renderer/example.vue.html index 8c06d7c..6276334 100644 --- a/packages/twoslash-vue/test/results/example.vue.html +++ b/packages/twoslash-vue/test/results/renderer/example.vue.html @@ -2,8 +2,7 @@ -

-<script setup lang="ts">
+
<script setup lang="ts">
 import { function ref<T>(value: T): Ref<UnwrapRef<T>> (+1 overload)
Takes an inner value and returns a reactive and mutable ref object, which has a single property `.value` that points to the inner value.
ref
,
const computed: { <T>(getter: ComputedGetter<T>, debugOptions?: DebuggerOptions | undefined): ComputedRef<T>; @@ -43,6 +42,6 @@ </script> <template> - <button: ButtonHTMLAttributes & ReservedPropsbutton @'click': ((payload: MouseEvent) => void) | undefinedclick="count: numbercount++">{{ msg: stringmsg }} Count is: {{ count: numbercount }}</button: ButtonHTMLAttributes & ReservedPropsbutton> + <button: ButtonHTMLAttributes & ReservedPropsbutton @
onClick?: ((payload: MouseEvent) => void) | undefined
click
="const count: Ref<number>count++">{{ msg: stringmsg }} Count is: {{ const count: Ref<number>count }}</button: ButtonHTMLAttributes & ReservedPropsbutton>
</template>
\ No newline at end of file diff --git a/packages/twoslash-vue/test/shikiji.test.ts b/packages/twoslash-vue/test/shikiji.test.ts index 7cee671..8ad7a20 100644 --- a/packages/twoslash-vue/test/shikiji.test.ts +++ b/packages/twoslash-vue/test/shikiji.test.ts @@ -3,42 +3,7 @@ import { codeToHtml } from 'shikiji' import { createTransformerFactory, rendererRich } from 'shikiji-twoslash/core' import { createTwoslasher } from '../src' -const code = ` - - - - - -` +const code = await import('./fixtures/example.vue?raw').then(m => m.default) const styleHeader = [ '', @@ -65,7 +30,7 @@ it('highlight vue', async () => { }) expect(styleHeader + result) - .toMatchFileSnapshot('./results/example.vue.html') + .toMatchFileSnapshot('./results/renderer/example.vue.html') }) const twoslasherRaw = createTwoslasher({ @@ -87,5 +52,5 @@ it('highlight raw', async () => { }) expect(styleHeader + result) - .toMatchFileSnapshot('./results/example.raw.html') + .toMatchFileSnapshot('./results/renderer/example.raw.html') }) diff --git a/packages/twoslash/src/core.ts b/packages/twoslash/src/core.ts index 29f8ffe..0227225 100644 --- a/packages/twoslash/src/core.ts +++ b/packages/twoslash/src/core.ts @@ -1,9 +1,8 @@ import type { CompilerOptions, CompletionEntry, CompletionTriggerKind, JsxEmit } from 'typescript' import { createFSBackedSystem, createSystem, createVirtualTypeScriptEnvironment } from '@typescript/vfs' -import { objectHash } from 'ohash' import { TwoslashError } from './error' import type { CreateTwoslashOptions, NodeError, NodeWithoutPosition, Position, Range, TwoslashExecuteOptions, TwoslashInstance, TwoslashOptions, TwoslashReturn, TwoslashReturnMeta, VirtualFile } from './types' -import { areRangesIntersecting, createPositionConverter, deExtensionify, findCutNotations, findFlagNotations, findQueryMarkers, getExtension, getIdentifierTextSpans, isInRange, isInRanges, removeCodeRanges, resolveNodePositions, splitFiles, typesToExtension } from './utils' +import { areRangesIntersecting, createPositionConverter, findCutNotations, findFlagNotations, findQueryMarkers, getExtension, getIdentifierTextSpans, isInRange, isInRanges, objectHash, removeCodeRanges, removeTsExtension, resolveNodePositions, splitFiles, typesToExtension } from './utils' import { validateCodeForErrors } from './validation' import { defaultCompilerOptions, defaultHandbookOptions } from './defaults' import type { CompilerOptionDeclaration } from './types/internal' @@ -265,14 +264,14 @@ export function createTwoslasher(createOptions: CreateTwoslashOptions = {}): Two // #region get completions for (const target of meta.positionCompletions) { - if (isInRemoval(target)) { + const file = getFileAtPosition(target)! + if (isInRemoval(target) || !file) { throw new TwoslashError( `Invalid completion query`, `The request on line ${pc.indexToPos(target).line + 2} for completions via ^| is in a removal range.`, `This is likely that the positioning is off.`, ) } - const file = getFileAtPosition(target)! let prefix = code.slice(0, target).match(/[$_\w]+$/)?.[0] || '' prefix = prefix.split('.').pop()! @@ -390,7 +389,7 @@ export function createTwoslasher(createOptions: CreateTwoslashOptions = {}): Two ? 'index.jsx' : 'index.js' - let emitSource = meta.virtualFiles.find(i => deExtensionify(i.filename) === deExtensionify(emitFilename))?.filename + let emitSource = meta.virtualFiles.find(i => removeTsExtension(i.filename) === removeTsExtension(emitFilename))?.filename if (!emitSource && !meta.compilerOptions.outFile) { const allFiles = meta.virtualFiles.map(i => i.filename).join(', ') diff --git a/packages/twoslash/src/public.ts b/packages/twoslash/src/public.ts index 87abe08..09a94d4 100644 --- a/packages/twoslash/src/public.ts +++ b/packages/twoslash/src/public.ts @@ -13,6 +13,8 @@ export { removeCodeRanges, resolveNodePositions, + + objectHash, } from './utils' export { diff --git a/packages/twoslash/src/utils.ts b/packages/twoslash/src/utils.ts index 44580b8..dd78485 100644 --- a/packages/twoslash/src/utils.ts +++ b/packages/twoslash/src/utils.ts @@ -5,6 +5,8 @@ import { defaultHandbookOptions } from './defaults' import type { CompilerOptionDeclaration } from './types/internal' import { reAnnonateMarkers, reConfigBoolean, reConfigValue, reCutAfter, reCutBefore, reCutEnd, reCutStart } from './regexp' +export { objectHash } from 'ohash' + export function parsePrimitive(value: string, type: string): any { // eslint-disable-next-line valid-typeof if (typeof value === type) @@ -252,7 +254,7 @@ export function resolveNodePositions(nodes: NodeWithoutPosition[], options: stri const resolved = nodes .filter(node => node.start >= 0) - .sort((a, b) => a.start - b.start) as TwoslashNode[] + .sort((a, b) => a.start - b.start || a.type.localeCompare(b.type)) as TwoslashNode[] resolved .forEach(node => Object.assign(node, indexToPos(node.start))) @@ -447,7 +449,7 @@ export function findQueryMarkers( } /** De-extension a filename, used for going from an output file to the source */ -export function deExtensionify(filename: string) { +export function removeTsExtension(filename: string) { // originally, .replace(".jsx", "").replace(".js", "").replace(".d.ts", "").replace(".map", "") const sansMapOrDTS = filename.replace(/\.d\.ts$/, '.ts').replace(/\.map$/, '') return sansMapOrDTS.replace(/\.[^/.]+$/, '') diff --git a/packages/twoslash/test/results/examples/compiler-errors.json b/packages/twoslash/test/results/examples/compiler-errors.json index 3559216..c305122 100644 --- a/packages/twoslash/test/results/examples/compiler-errors.json +++ b/packages/twoslash/test/results/examples/compiler-errors.json @@ -10,15 +10,6 @@ "line": 1, "character": 9 }, - { - "type": "hover", - "text": "(parameter) s: any", - "start": 13, - "length": 1, - "target": "s", - "line": 1, - "character": 12 - }, { "type": "error", "start": 13, @@ -31,6 +22,15 @@ "line": 1, "character": 12 }, + { + "type": "hover", + "text": "(parameter) s: any", + "start": 13, + "length": 1, + "target": "s", + "line": 1, + "character": 12 + }, { "type": "hover", "text": "var console: Console", diff --git a/packages/twoslash/test/results/examples/errors-with-generics.json b/packages/twoslash/test/results/examples/errors-with-generics.json index a982cd0..a0f7fb1 100644 --- a/packages/twoslash/test/results/examples/errors-with-generics.json +++ b/packages/twoslash/test/results/examples/errors-with-generics.json @@ -39,15 +39,6 @@ "line": 1, "character": 7 }, - { - "type": "hover", - "text": "let b: Record", - "start": 72, - "length": 1, - "target": "b", - "line": 2, - "character": 0 - }, { "type": "error", "start": 72, @@ -60,6 +51,15 @@ "line": 2, "character": 0 }, + { + "type": "hover", + "text": "let b: Record", + "start": 72, + "length": 1, + "target": "b", + "line": 2, + "character": 0 + }, { "type": "hover", "text": "const a: Record", diff --git a/packages/twoslash/test/results/examples/highlighting.json b/packages/twoslash/test/results/examples/highlighting.json index ffa238c..0cdb554 100644 --- a/packages/twoslash/test/results/examples/highlighting.json +++ b/packages/twoslash/test/results/examples/highlighting.json @@ -95,7 +95,7 @@ "character": 0 }, { - "type": "hover", + "type": "highlight", "text": "var Date: DateConstructor\nnew () => Date (+4 overloads)", "start": 138, "length": 4, @@ -104,7 +104,7 @@ "character": 22 }, { - "type": "highlight", + "type": "hover", "text": "var Date: DateConstructor\nnew () => Date (+4 overloads)", "start": 138, "length": 4, diff --git a/packages/twoslash/test/results/tests/cut-file-errors.json b/packages/twoslash/test/results/tests/cut-file-errors.json index 6b98eb6..d77c2b8 100644 --- a/packages/twoslash/test/results/tests/cut-file-errors.json +++ b/packages/twoslash/test/results/tests/cut-file-errors.json @@ -73,15 +73,6 @@ "line": 8, "character": 2 }, - { - "type": "hover", - "text": "any", - "start": 327, - "length": 4, - "target": "code", - "line": 8, - "character": 8 - }, { "type": "error", "start": 327, @@ -94,6 +85,15 @@ "line": 8, "character": 8 }, + { + "type": "hover", + "text": "any", + "start": 327, + "length": 4, + "target": "code", + "line": 8, + "character": 8 + }, { "type": "hover", "text": "(parameter) state: NetworkState", diff --git a/packages/twoslash/test/results/tests/files-with-json.json b/packages/twoslash/test/results/tests/files-with-json.json index f4d49e2..4a9444e 100644 --- a/packages/twoslash/test/results/tests/files-with-json.json +++ b/packages/twoslash/test/results/tests/files-with-json.json @@ -28,15 +28,6 @@ "line": 9, "character": 9 }, - { - "type": "hover", - "text": "import settings", - "start": 184, - "length": 8, - "target": "settings", - "line": 10, - "character": 0 - }, { "type": "error", "start": 184, @@ -49,6 +40,15 @@ "line": 10, "character": 0 }, + { + "type": "hover", + "text": "import settings", + "start": 184, + "length": 8, + "target": "settings", + "line": 10, + "character": 0 + }, { "type": "hover", "text": "(property) \"dry\": boolean", diff --git a/packages/twoslash/test/results/tests/inline-highlights.json b/packages/twoslash/test/results/tests/inline-highlights.json index 3097117..598c542 100644 --- a/packages/twoslash/test/results/tests/inline-highlights.json +++ b/packages/twoslash/test/results/tests/inline-highlights.json @@ -2,7 +2,7 @@ "code": "type Result = \"pass\" | \"fail\"\n\nconst hello = \"OK\"\n", "nodes": [ { - "type": "hover", + "type": "highlight", "text": "type Result = \"pass\" | \"fail\"", "start": 5, "length": 6, @@ -11,7 +11,7 @@ "character": 5 }, { - "type": "highlight", + "type": "hover", "text": "type Result = \"pass\" | \"fail\"", "start": 5, "length": 6, @@ -20,7 +20,7 @@ "character": 5 }, { - "type": "hover", + "type": "highlight", "text": "const hello: \"OK\"", "start": 37, "length": 5, @@ -29,7 +29,7 @@ "character": 6 }, { - "type": "highlight", + "type": "hover", "text": "const hello: \"OK\"", "start": 37, "length": 5, diff --git a/packages/twoslash/test/results/tests/large-cut.json b/packages/twoslash/test/results/tests/large-cut.json index debee24..793e5dc 100644 --- a/packages/twoslash/test/results/tests/large-cut.json +++ b/packages/twoslash/test/results/tests/large-cut.json @@ -73,15 +73,6 @@ "line": 8, "character": 2 }, - { - "type": "hover", - "text": "any", - "start": 337, - "length": 4, - "target": "code", - "line": 8, - "character": 8 - }, { "type": "error", "start": 337, @@ -94,6 +85,15 @@ "line": 8, "character": 8 }, + { + "type": "hover", + "text": "any", + "start": 337, + "length": 4, + "target": "code", + "line": 8, + "character": 8 + }, { "type": "hover", "text": "(parameter) state: NetworkState", diff --git a/packages/twoslash/test/utils.test.ts b/packages/twoslash/test/utils.test.ts index d5e6b80..a0ec005 100644 --- a/packages/twoslash/test/utils.test.ts +++ b/packages/twoslash/test/utils.test.ts @@ -1,6 +1,6 @@ import ts from 'typescript' import { expect, it } from 'vitest' -import { deExtensionify, getIdentifierTextSpans } from '../src/utils' +import { getIdentifierTextSpans, removeTsExtension } from '../src/utils' it('gets the expected identifiers', () => { const file = ts.createSourceFile( @@ -33,10 +33,10 @@ readdirSync(fixturesFolder).forEach(fixtureName => { }) it('reduces filenames down', () => { - expect(deExtensionify('foo.ts')).toEqual('foo') - expect(deExtensionify('foo.tsx')).toEqual('foo') - expect(deExtensionify('foo.d.ts')).toEqual('foo') - expect(deExtensionify('foo.js.map')).toEqual('foo') - expect(deExtensionify('foo')).toEqual('foo') - expect(deExtensionify('foo.vue')).toEqual('foo') + expect(removeTsExtension('foo.ts')).toEqual('foo') + expect(removeTsExtension('foo.tsx')).toEqual('foo') + expect(removeTsExtension('foo.d.ts')).toEqual('foo') + expect(removeTsExtension('foo.js.map')).toEqual('foo') + expect(removeTsExtension('foo')).toEqual('foo') + expect(removeTsExtension('foo.vue')).toEqual('foo') })