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.
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import { Project, SourceFile } from 'ts-morph'
2
+
3
+ export interface TransformContext {
4
+ file: SourceFile
5
+ project: Project
6
+ }
7
+
8
+ export type Transform = (ctx: TransformContext) => void