Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: introduce twoslash-protocol package #19

Merged
merged 4 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"unbuild": "^2.0.0",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.0",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.2.2",
"vue": "^3.4.16"
},
Expand Down
18 changes: 18 additions & 0 deletions packages/twoslash-protocol/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
The MIT License (MIT)

Copyright (c) 2024-PRESENT Anthony Fu <https://github.com/antfu>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 changes: 30 additions & 0 deletions packages/twoslash-protocol/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<img src="https://twoslash.netlify.app/logo.svg" alt="Twoslash Logo" width="150">

# twoslash-protocol

[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href]
[![JSDocs][jsdocs-src]][jsdocs-href]
[![License][license-src]][license-href]

[📚 Documentation](https://twoslash.netlify.app/)

The protocol for Twoslash interface, a universal layer to support different language services that describe common editor features. This package also includes some common utilities for working with the protocol.

## License

MIT License © [Anthony Fu](https://github.com/antfu

<!-- Badges -->

[npm-version-src]: https://img.shields.io/npm/v/twoslash-protocol?style=flat&colorA=161514&colorB=EAB836
[npm-version-href]: https://npmjs.com/package/twoslash-protocol
[npm-downloads-src]: https://img.shields.io/npm/dm/twoslash-protocol?style=flat&colorA=161514&colorB=E66041
[npm-downloads-href]: https://npmjs.com/package/twoslash-protocol
[bundle-src]: https://img.shields.io/bundlephobia/minzip/twoslash-protocol?style=flat&colorA=161514&colorB=45627B&label=minzip
[bundle-href]: https://bundlephobia.com/result?p=twoslash-protocol
[license-src]: https://img.shields.io/github/license/twoslashes/twoslash.svg?style=flat&colorA=161514&colorB=45627B
[license-href]: https://github.com/twoslashes/twoslash/blob/main/LICENSE
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-161514?style=flat&colorA=161514&colorB=EAB836
[jsdocs-href]: https://www.jsdocs.io/package/twoslash-protocol
13 changes: 13 additions & 0 deletions packages/twoslash-protocol/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineBuildConfig } from 'unbuild'

export default defineBuildConfig({
entries: [
'src/index',
'src/types',
],
declaration: true,
clean: true,
rollup: {
emitCJS: true,
},
})
50 changes: 50 additions & 0 deletions packages/twoslash-protocol/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "twoslash-protocol",
"type": "module",
"version": "0.1.2",
"description": "The protocol for the Twoslash interface",
"author": "Anthony Fu",
"license": "MIT",
"homepage": "https://github.com/twoslashes/twoslash",
"repository": {
"url": "https://github.com/twoslashes/twoslash",
"type": "git",
"directory": "packages/twoslash-protocol"
},
"bugs": {
"url": "https://github.com/twoslashes/twoslash/issues"
},
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./types": {
"types": "./dist/types.d.mts",
"import": "./dist/types.mjs",
"require": "./dist/types.cjs"
}
},
"main": "dist/index.mjs",
"module": "dist/index.mjs",
"types": "dist/index.d.mts",
"typesVersions": {
"*": {
"./types": ["./dist/types.d.mts"],
"*": ["./dist/index.d.mts"]
}
},
"files": [
"dist"
],
"scripts": {
"build": "unbuild",
"prepublishOnly": "nr build"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
}
}
2 changes: 2 additions & 0 deletions packages/twoslash-protocol/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types'
export * from './utils'
1 change: 1 addition & 0 deletions packages/twoslash-protocol/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types/index'
2 changes: 2 additions & 0 deletions packages/twoslash-protocol/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './nodes'
export * from './returns'
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { CompletionEntry } from 'typescript'

/**
* Basic node with start and length to represent a range in the code
*/
Expand Down Expand Up @@ -36,6 +34,11 @@ export interface NodeQuery extends Omit<NodeHover, 'type'> {
type: 'query'
}

export interface CompletionEntry {
name: string
kind?: string
}

export interface NodeCompletion extends NodeBase {
type: 'completion'
/** Results for completions at a particular point */
Expand All @@ -44,22 +47,20 @@ export interface NodeCompletion extends NodeBase {
completionsPrefix: string
}

export type ErrorLevel = 'warning' | 'error' | 'suggestion' | 'message'

export interface NodeError extends NodeBase {
type: 'error'
id?: string
/**
* Error level:
*
* Warning = 0
* Error = 1
* Suggestion = 2
* Message = 3
* Error level
* When not provided, defaults to 'error'
*/
level?: 0 | 1 | 2 | 3
level?: ErrorLevel
/**
* Error code
*/
code: number
code?: number | string
/**
* Error message
*/
Expand Down
20 changes: 20 additions & 0 deletions packages/twoslash-protocol/src/types/returns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { TwoslashNode } from './nodes'

export interface TwoslashGenericResult {
/**
* The output code, could be TypeScript, but could also be a JS/JSON/d.ts
*/
code: string

/**
* Extension of the output code
*/
extension?: string

/**
* Nodes containing various bits of information about the code
*/
nodes: TwoslashNode[]
}

export type TwoslashGenericFunction<Options> = (code: string, filename?: string, options?: Options) => TwoslashGenericResult
139 changes: 139 additions & 0 deletions packages/twoslash-protocol/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import type { NodeStartLength, NodeWithoutPosition, Position, Range, TwoslashNode } from './types'

export function isInRange(index: number, range: Range) {
return range[0] <= index && index <= range[1]
}

export function isInRanges(index: number, ranges: Range[]) {
return ranges.find(range => isInRange(index, range))
}

/**
* Merge overlapping ranges
*/
export function mergeRanges(ranges: Range[]) {
ranges.sort((a, b) => a[0] - b[0])
const merged: Range[] = []
for (const range of ranges) {
const last = merged[merged.length - 1]
if (last && last[1] >= range[0])
last[1] = Math.max(last[1], range[1])
else
merged.push(range)
}
return merged
}

/**
* Slipt a string into lines, each line preserves the line ending.
*/
export function splitLines(code: string, preserveEnding = false): [string, number][] {
const parts = code.split(/(\r?\n)/g)
let index = 0
const lines: [string, number][] = []
for (let i = 0; i < parts.length; i += 2) {
const line = preserveEnding
? parts[i] + (parts[i + 1] || '')
: parts[i]

Check warning on line 37 in packages/twoslash-protocol/src/utils.ts

View check run for this annotation

Codecov / codecov/patch

packages/twoslash-protocol/src/utils.ts#L37

Added line #L37 was not covered by tests
lines.push([line, index])
index += parts[i].length
index += parts[i + 1]?.length || 0
}
return lines
}

/**
* Creates a converter between index and position in a code block.
*/
export function createPositionConverter(code: string) {
const lines = splitLines(code, true).map(([line]) => line)

function indexToPos(index: number): Position {
let character = index
let line = 0
for (const lineText of lines) {
if (character < lineText.length)
break
character -= lineText.length
line++
}
return { line, character }
}

function posToIndex(line: number, character: number) {
let index = 0
for (let i = 0; i < line; i++)
index += lines[i].length

index += character
return index
}

return {
lines,
indexToPos,
posToIndex,
}
}

/**
* Remove ranages for a string, and update nodes' `start` property accordingly
*
* Note that items in `nodes` will be mutated
*/
export function removeCodeRanges<T extends NodeStartLength>(code: string, removals: Range[], nodes: T[]): { code: string, removals: Range[], nodes: T[] }
export function removeCodeRanges(code: string, removals: Range[]): { code: string, removals: Range[], nodes: undefined }
export function removeCodeRanges(code: string, removals: Range[], nodes?: NodeStartLength[]) {
// Sort descending, so that we start removal from the end
const ranges = mergeRanges(removals)
.sort((a, b) => b[0] - a[0])

let outputCode = code
for (const remove of ranges) {
const removalLength = remove[1] - remove[0]
outputCode = outputCode.slice(0, remove[0]) + outputCode.slice(remove[1])
nodes?.forEach((node) => {
// nodes before the range, do nothing
if (node.start + node.length <= remove[0])
return undefined

// remove nodes that are within in the range
else if (node.start < remove[1])
node.start = -1

// move nodes after the range forward
else
node.start -= removalLength
})
}

return {
code: outputCode,
removals: ranges,
nodes,
}
}

/**
* - Calculate nodes `line` and `character` properties to match the code
* - Remove nodes that has negative `start` property
* - Sort nodes by `start`
*
* Note that the nodes items will be mutated, clone them beforehand if not desired
*/
export function resolveNodePositions(nodes: NodeWithoutPosition[], code: string): TwoslashNode[]
export function resolveNodePositions(nodes: NodeWithoutPosition[], indexToPos: (index: number) => Position): TwoslashNode[]
export function resolveNodePositions(nodes: NodeWithoutPosition[], options: string | ((index: number) => Position)): TwoslashNode[] {
const indexToPos = typeof options === 'string'
? createPositionConverter(options).indexToPos
: options

const resolved = nodes
.filter(node => node.start >= 0)
.sort((a, b) => a.start - b.start || a.type.localeCompare(b.type)) as TwoslashNode[]

resolved
.forEach(node => Object.assign(node, indexToPos(node.start)))

return resolved
}
3 changes: 2 additions & 1 deletion packages/twoslash-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"dependencies": {
"@vue/language-core": "^1.8.27",
"twoslash": "workspace:*"
"twoslash": "workspace:*",
"twoslash-protocol": "workspace:*"
}
}
6 changes: 4 additions & 2 deletions packages/twoslash-vue/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ import type {
TwoslashReturnMeta,
} from 'twoslash'
import {
createPositionConverter,
createTwoslasher as createTwoslasherBase,
defaultCompilerOptions,
defaultHandbookOptions,
findFlagNotations,
findQueryMarkers,
getObjectHash,
} from 'twoslash'
import {
createPositionConverter,
removeCodeRanges,
resolveNodePositions,
} from 'twoslash'
} from 'twoslash-protocol'

export interface VueSpecificOptions {
/**
Expand Down
3 changes: 2 additions & 1 deletion packages/twoslash/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"typescript": "*"
},
"dependencies": {
"@typescript/vfs": "1.5.0"
"@typescript/vfs": "1.5.0",
"twoslash-protocol": "workspace:*"
},
"devDependencies": {
"ohash": "^1.1.3"
Expand Down
Loading
Loading