coc-vscode-loader 1.1.1 → 1.1.4
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.
- package/README.md +19 -6
- package/assets/tui-preview.png +0 -0
- package/converter/src/cli.ts +28 -0
- package/converter/src/convert.ts +501 -0
- package/converter/src/presets.ts +57 -0
- package/converter/src/scanner.ts +136 -0
- package/converter/src/transforms/class-to-factory.ts +54 -0
- package/converter/src/transforms/enum-offset.ts +49 -0
- package/converter/src/transforms/import-mapping.ts +47 -0
- package/converter/src/transforms/language-client.ts +48 -0
- package/converter/src/transforms/provider-register.ts +55 -0
- package/converter/src/types.ts +8 -0
- package/lib/index.js +536 -103
- package/package.json +7 -1
- package/converter/README.md +0 -134
- package/converter/package-lock.json +0 -693
- package/converter/pnpm-lock.yaml +0 -419
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as fs from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
|
|
4
|
+
export interface ScanResult {
|
|
5
|
+
files: ScannedFile[]
|
|
6
|
+
hasTsBridge: boolean
|
|
7
|
+
hasDecoration: boolean
|
|
8
|
+
hasWebview: boolean
|
|
9
|
+
summary: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ScannedFile {
|
|
13
|
+
path: string
|
|
14
|
+
apis: string[]
|
|
15
|
+
actions: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const UNSUPPORTED_PATTERNS = [
|
|
19
|
+
{ pattern: 'createTextEditorDecorationType', action: 'mark-unsupported', label: 'decoration API' },
|
|
20
|
+
{ pattern: 'setDecorations', action: 'mark-unsupported', label: 'decoration API' },
|
|
21
|
+
{ pattern: 'createWebviewPanel', action: 'mark-unsupported', label: 'webview API' },
|
|
22
|
+
{ pattern: 'registerTreeDataProvider', action: 'mark-unsupported', label: 'tree data provider' },
|
|
23
|
+
{ pattern: 'window.showInputBox', action: 'needs-rewrite', label: 'use requestInput instead' },
|
|
24
|
+
{ pattern: 'env.openExternal', action: 'mark-unsupported', label: 'no equivalent' },
|
|
25
|
+
{ pattern: 'showOpenDialog', action: 'mark-unsupported', label: 'no equivalent' },
|
|
26
|
+
{ pattern: 'showSaveDialog', action: 'mark-unsupported', label: 'no equivalent' },
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
const TS_BRIDGE_PATTERNS = [
|
|
30
|
+
'tsserver/request',
|
|
31
|
+
'tsserver/response',
|
|
32
|
+
'_vue:',
|
|
33
|
+
'typescript.tsserverRequest',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export function scan(dir: string): ScanResult {
|
|
37
|
+
const files: ScannedFile[] = []
|
|
38
|
+
let hasTsBridge = false
|
|
39
|
+
let hasDecoration = false
|
|
40
|
+
let hasWebview = false
|
|
41
|
+
|
|
42
|
+
const tsFiles = walk(dir).filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
|
|
43
|
+
|
|
44
|
+
for (const filePath of tsFiles) {
|
|
45
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
46
|
+
const apis: string[] = []
|
|
47
|
+
const actions: string[] = []
|
|
48
|
+
const relative = path.relative(dir, filePath)
|
|
49
|
+
|
|
50
|
+
// Check for vscode imports
|
|
51
|
+
if (content.includes("from 'vscode'") || content.includes('from "vscode"') || content.includes('require("vscode")')) {
|
|
52
|
+
apis.push('vscode')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for unsupported patterns
|
|
56
|
+
for (const { pattern, action, label } of UNSUPPORTED_PATTERNS) {
|
|
57
|
+
if (content.includes(pattern)) {
|
|
58
|
+
apis.push(label)
|
|
59
|
+
actions.push(action)
|
|
60
|
+
if (action === 'mark-unsupported') {
|
|
61
|
+
if (label.includes('decoration')) hasDecoration = true
|
|
62
|
+
if (label.includes('webview')) hasWebview = true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for TS bridge
|
|
68
|
+
for (const pattern of TS_BRIDGE_PATTERNS) {
|
|
69
|
+
if (content.includes(pattern)) {
|
|
70
|
+
hasTsBridge = true
|
|
71
|
+
apis.push('tsserver bridge')
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for LanguageClient
|
|
77
|
+
if (content.includes('LanguageClient')) {
|
|
78
|
+
apis.push('LanguageClient')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check for typescriptServerPlugins in package.json
|
|
82
|
+
if (relative === 'package.json' || filePath.endsWith('package.json')) {
|
|
83
|
+
if (content.includes('typescriptServerPlugins')) {
|
|
84
|
+
hasTsBridge = true
|
|
85
|
+
apis.push('typescriptServerPlugins')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (apis.length > 0) {
|
|
90
|
+
files.push({ path: relative, apis, actions })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Read package.json
|
|
95
|
+
const pkgPath = path.join(dir, 'package.json')
|
|
96
|
+
if (fs.existsSync(pkgPath)) {
|
|
97
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
98
|
+
const apis: string[] = []
|
|
99
|
+
if (pkg.contributes?.typescriptServerPlugins) {
|
|
100
|
+
hasTsBridge = true
|
|
101
|
+
apis.push('typescriptServerPlugins')
|
|
102
|
+
}
|
|
103
|
+
if (pkg.activationEvents) {
|
|
104
|
+
apis.push(`activationEvents: ${pkg.activationEvents.length}`)
|
|
105
|
+
}
|
|
106
|
+
if (apis.length > 0) {
|
|
107
|
+
files.push({ path: 'package.json', apis, actions: [] })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
files,
|
|
113
|
+
hasTsBridge,
|
|
114
|
+
hasDecoration,
|
|
115
|
+
hasWebview,
|
|
116
|
+
summary: [
|
|
117
|
+
`found ${files.length} files with vscode API`,
|
|
118
|
+
hasTsBridge ? ', ts-bridge detected' : '',
|
|
119
|
+
hasDecoration ? ', decoration API (marked)' : '',
|
|
120
|
+
hasWebview ? ', webview API (marked)' : '',
|
|
121
|
+
].join(''),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function walk(dir: string): string[] {
|
|
126
|
+
const files: string[] = []
|
|
127
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
128
|
+
const p = path.join(dir, entry.name)
|
|
129
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
|
|
130
|
+
files.push(...walk(p))
|
|
131
|
+
} else if (entry.isFile()) {
|
|
132
|
+
files.push(p)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return files
|
|
136
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Transform } from '../types.js'
|
|
2
|
+
import { SyntaxKind } from 'ts-morph'
|
|
3
|
+
|
|
4
|
+
const FACTORY_TYPES = new Set([
|
|
5
|
+
'Position', 'Range', 'Location', 'LocationLink',
|
|
6
|
+
'Diagnostic', 'DiagnosticRelatedInformation',
|
|
7
|
+
'TextEdit',
|
|
8
|
+
'Hover', 'CompletionItem', 'CompletionList',
|
|
9
|
+
'CodeAction', 'CodeLens', 'DocumentLink',
|
|
10
|
+
'Color', 'ColorInformation', 'ColorPresentation',
|
|
11
|
+
'FoldingRange', 'SelectionRange',
|
|
12
|
+
'DocumentHighlight', 'SymbolInformation', 'DocumentSymbol',
|
|
13
|
+
'ParameterInformation', 'SignatureInformation',
|
|
14
|
+
'CallHierarchyItem', 'CallHierarchyIncomingCall', 'CallHierarchyOutgoingCall',
|
|
15
|
+
'TypeHierarchyItem', 'LinkedEditingRanges',
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
export const transformClassToFactory: Transform = (ctx) => {
|
|
19
|
+
const { file } = ctx
|
|
20
|
+
|
|
21
|
+
// AST approach: try to replace via ts-morph
|
|
22
|
+
const nodes = file.getDescendantsOfKind(SyntaxKind.NewExpression)
|
|
23
|
+
const astReplacements: Array<{ node: any, text: string }> = []
|
|
24
|
+
for (const expr of nodes) {
|
|
25
|
+
const text = expr.getText()
|
|
26
|
+
const m = text.match(/^new\s+(\w+)\(/)
|
|
27
|
+
if (!m || !FACTORY_TYPES.has(m[1])) continue
|
|
28
|
+
const args = text.slice(m[0].length, -1)
|
|
29
|
+
astReplacements.push({ node: expr, text: `${m[1]}.create(${args})` })
|
|
30
|
+
}
|
|
31
|
+
for (const { node, text } of astReplacements) {
|
|
32
|
+
try { node.replaceWithText(text) } catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Text fallback: catch remaining new Xxx() that AST might have missed
|
|
36
|
+
let text = file.getText()
|
|
37
|
+
text = text.replace(
|
|
38
|
+
/\bnew\s+(Position|Range|Location|Diagnostic|TextEdit)\s*\(/g,
|
|
39
|
+
(match, type) => `${type}.create(`
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// CompletionItem.create(label) doesn't accept kind in coc.
|
|
43
|
+
// Convert `CompletionItem.create(label, kind)` to `item = CompletionItem.create(label); item.kind = kind`
|
|
44
|
+
text = text.replace(
|
|
45
|
+
/const\s+(\w+)\s*=\s*CompletionItem\.create\(([^,]+),\s*([^)]+)\)/g,
|
|
46
|
+
(_, varName, label, kind) => {
|
|
47
|
+
return `const ${varName} = CompletionItem.create(${label}); ${varName}.kind = ${kind}`
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if (text !== file.getText()) {
|
|
52
|
+
file.replaceWithText(text)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Transform } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handle enum value offsets between VS Code (0-based) and coc (1-based LSP).
|
|
5
|
+
* Some enums like DiagnosticSeverity, CompletionItemKind, SymbolKind have
|
|
6
|
+
* different numeric values, but since coc re-exports them with correct values,
|
|
7
|
+
* symbol references (like CompletionItemKind.Value) work correctly at runtime.
|
|
8
|
+
*
|
|
9
|
+
* This transform handles cases where hardcoded numbers are used instead of
|
|
10
|
+
* enum symbols, which is rare but can happen in extensions.
|
|
11
|
+
*
|
|
12
|
+
* Affected enums and their offset:
|
|
13
|
+
* CompletionItemKind: vscode Text=0 → coc Text:1 (differs by 1 for first ~11 values)
|
|
14
|
+
* SymbolKind: vscode File=0 → coc File:1
|
|
15
|
+
* DocumentHighlightKind: vscode Text=0 → coc Text:1
|
|
16
|
+
* DiagnosticSeverity: vscode Error=0 → coc Error:1
|
|
17
|
+
*/
|
|
18
|
+
export const transformEnumOffset: Transform = (ctx) => {
|
|
19
|
+
const { file } = ctx
|
|
20
|
+
let content = file.getText()
|
|
21
|
+
|
|
22
|
+
// Detect hardcoded numbers used in enum position (e.g., CompletionItemKind.Xxx).
|
|
23
|
+
// This is hard to detect perfectly, so we log a note when numeric literals
|
|
24
|
+
// appear near enum-type names.
|
|
25
|
+
const enumPatterns = [
|
|
26
|
+
'CompletionItemKind', 'SymbolKind', 'DocumentHighlightKind', 'DiagnosticSeverity',
|
|
27
|
+
'CompletionTriggerKind', 'InlineCompletionTriggerKind',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
for (const enumName of enumPatterns) {
|
|
31
|
+
// Check if the enum is imported/used with a hardcoded number nearby
|
|
32
|
+
const enumRefs = content.match(new RegExp(`${enumName}\\.\\w+`, 'g'))
|
|
33
|
+
if (enumRefs) {
|
|
34
|
+
// Symbol references are fine - they resolve at runtime
|
|
35
|
+
// Only note if there are raw numbers being compared
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Replace any numeric enum comparisons with comments
|
|
40
|
+
// e.g., `severity === 0` → `severity === 0 /* DiagnosticSeverity.Error = 1 in coc */`
|
|
41
|
+
content = content.replace(
|
|
42
|
+
/(severity\s*[=!]==?\s*)(\d+)/g,
|
|
43
|
+
'$1$2 /* DiagnosticSeverity values differ in coc (1-4 vs 0-3) */'
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (content !== file.getText()) {
|
|
47
|
+
file.replaceWithText(content)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Transform } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Replace `from 'vscode'` with `from 'coc.nvim'`,
|
|
5
|
+
* and apply name remapping for known API differences.
|
|
6
|
+
*/
|
|
7
|
+
const MAPPINGS: Record<string, string> = {
|
|
8
|
+
// namespace
|
|
9
|
+
'vscode': 'coc.nvim',
|
|
10
|
+
|
|
11
|
+
// naming differences
|
|
12
|
+
'EventEmitter': 'Emitter',
|
|
13
|
+
'Disposable': 'Disposable',
|
|
14
|
+
|
|
15
|
+
// function/method renames
|
|
16
|
+
'getExtension': 'getExtensionById',
|
|
17
|
+
'registerReferenceProvider': 'registerReferencesProvider',
|
|
18
|
+
'registerCodeActionsProvider': 'registerCodeActionProvider',
|
|
19
|
+
'registerColorProvider': 'registerDocumentColorProvider',
|
|
20
|
+
'registerDocumentFormattingEditProvider': 'registerDocumentFormatProvider',
|
|
21
|
+
'registerDocumentRangeFormattingEditProvider': 'registerDocumentRangeFormatProvider',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const transformImportMapping: Transform = (ctx) => {
|
|
25
|
+
const { file } = ctx
|
|
26
|
+
|
|
27
|
+
// Rewrite import declarations
|
|
28
|
+
file.getImportDeclarations().forEach(decl => {
|
|
29
|
+
const mod = decl.getModuleSpecifierValue()
|
|
30
|
+
if (mod === 'vscode') {
|
|
31
|
+
decl.setModuleSpecifier('coc.nvim')
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Rewrite named references
|
|
36
|
+
file.getDescendantsOfKind(192 /* Identifier */).forEach(node => {
|
|
37
|
+
const text = node.getText()
|
|
38
|
+
const mapped = MAPPINGS[text]
|
|
39
|
+
if (mapped && mapped !== text) {
|
|
40
|
+
// Only replace if it's a direct reference, not part of a string
|
|
41
|
+
const parent = node.getParent()
|
|
42
|
+
if (parent && parent.getKindName() !== 'StringLiteral') {
|
|
43
|
+
node.replaceWithText(mapped)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Transform } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adapt LanguageClient construction from VS Code style to coc style.
|
|
5
|
+
*
|
|
6
|
+
* VS Code:
|
|
7
|
+
* new LanguageClient('id', 'name', serverOptions, clientOptions)
|
|
8
|
+
* serverOptions = { run: { module, transport }, debug: { module, transport } }
|
|
9
|
+
*
|
|
10
|
+
* coc:
|
|
11
|
+
* new LanguageClient('id', 'name', serverOptions, clientOptions)
|
|
12
|
+
* serverOptions = { module, transport, options? }
|
|
13
|
+
*/
|
|
14
|
+
export const transformLanguageClient: Transform = (ctx) => {
|
|
15
|
+
const { file } = ctx
|
|
16
|
+
|
|
17
|
+
file.getDescendantsOfKind(199 /* CallExpression */).forEach(call => {
|
|
18
|
+
const text = call.getText()
|
|
19
|
+
|
|
20
|
+
// Match: new LanguageClient(...)
|
|
21
|
+
if (!text.startsWith('new LanguageClient(')) return
|
|
22
|
+
|
|
23
|
+
// Extract serverOptions argument (3rd positional arg)
|
|
24
|
+
const args = call.getArguments()
|
|
25
|
+
if (args.length < 3) return
|
|
26
|
+
|
|
27
|
+
const serverOpts = args[2].getText()
|
|
28
|
+
|
|
29
|
+
// Check if it has the VS Code style { run, debug } structure
|
|
30
|
+
if (!serverOpts.includes('run:') || !serverOpts.includes('debug:')) return
|
|
31
|
+
|
|
32
|
+
// Extract module and transport from run block
|
|
33
|
+
const moduleMatch = serverOpts.match(/module:\s*(\S+)/)
|
|
34
|
+
const transportMatch = serverOpts.match(/transport:\s*(\S+)/)
|
|
35
|
+
|
|
36
|
+
if (!moduleMatch) return
|
|
37
|
+
|
|
38
|
+
// Build coc-style serverOptions
|
|
39
|
+
let cocOpts = `{\n module: ${moduleMatch[1]}`
|
|
40
|
+
if (transportMatch) {
|
|
41
|
+
cocOpts += `,\n transport: ${transportMatch[1]}`
|
|
42
|
+
}
|
|
43
|
+
cocOpts += '\n }'
|
|
44
|
+
|
|
45
|
+
// Replace the argument
|
|
46
|
+
args[2].replaceWithText(cocOpts)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Transform } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adapt provider registration function signatures.
|
|
5
|
+
*
|
|
6
|
+
* registerCompletionItemProvider(sel, p, t) → registerCompletionItemProvider('name', 'sc', sel, p, [t])
|
|
7
|
+
* registerCodeActionsProvider(sel, p, m?) → registerCodeActionProvider(sel, p, clientId?, kinds?)
|
|
8
|
+
* registerReferenceProvider(sel, p) → registerReferencesProvider(sel, p)
|
|
9
|
+
* registerDocumentFormattingEditProvider → registerDocumentFormatProvider(sel, p, priority?)
|
|
10
|
+
* registerColorProvider → registerDocumentColorProvider(sel, p)
|
|
11
|
+
*/
|
|
12
|
+
const RENAMES: Record<string, string> = {
|
|
13
|
+
registerCodeActionsProvider: 'registerCodeActionProvider',
|
|
14
|
+
registerReferenceProvider: 'registerReferencesProvider',
|
|
15
|
+
registerDocumentFormattingEditProvider: 'registerDocumentFormatProvider',
|
|
16
|
+
registerDocumentRangeFormattingEditProvider: 'registerDocumentRangeFormatProvider',
|
|
17
|
+
registerColorProvider: 'registerDocumentColorProvider',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const transformProviderRegister: Transform = (ctx) => {
|
|
21
|
+
let { file } = ctx
|
|
22
|
+
let content = file.getText()
|
|
23
|
+
let changed = false
|
|
24
|
+
|
|
25
|
+
// 1. Simple renames
|
|
26
|
+
for (const [from, to] of Object.entries(RENAMES)) {
|
|
27
|
+
const re = new RegExp(`\\b${from}\\b`, 'g')
|
|
28
|
+
if (re.test(content)) {
|
|
29
|
+
content = content.replace(re, to)
|
|
30
|
+
changed = true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. registerCompletionItemProvider: insert name + shortcut at beginning
|
|
35
|
+
if (content.includes('registerCompletionItemProvider')) {
|
|
36
|
+
content = content.replace(
|
|
37
|
+
/registerCompletionItemProvider\(/g,
|
|
38
|
+
`registerCompletionItemProvider('plugin', 'PL', `
|
|
39
|
+
)
|
|
40
|
+
// Wrap the last argument in an array if it's a string (trigger chars)
|
|
41
|
+
content = content.replace(
|
|
42
|
+
/(registerCompletionItemProvider\([^)]+),\s*'([^']+)'\)/g,
|
|
43
|
+
'$1, ["$2"])'
|
|
44
|
+
)
|
|
45
|
+
content = content.replace(
|
|
46
|
+
/(registerCompletionItemProvider\([^)]+),\s*"([^"]+)"\)/g,
|
|
47
|
+
'$1, ["$2"])'
|
|
48
|
+
)
|
|
49
|
+
changed = true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (changed) {
|
|
53
|
+
file.replaceWithText(content)
|
|
54
|
+
}
|
|
55
|
+
}
|