From 2ef5bbf7807003b299cc907b5d14375ceb3e24a1 Mon Sep 17 00:00:00 2001 From: Ugur Aslan Date: Wed, 25 Sep 2024 21:40:40 +0200 Subject: [PATCH] implemented editor features for js/ts content in .blits file --- src/blitsFile/codeActionsProvider.js | 66 ++++++++ src/blitsFile/completionProvider.js | 77 +++++++++ src/blitsFile/config/compilerOptions.js | 51 ++++++ src/blitsFile/diagnostics.js | 107 +++++++++++++ src/blitsFile/hoverProvider.js | 53 ++++++ src/blitsFile/languageService.js | 195 +++++++++++++++++++++++ src/blitsFile/signatureHelpProvider.js | 80 ++++++++++ src/blitsFile/utils/fileNameGenerator.js | 43 +++++ src/blitsFile/utils/scriptExtractor.js | 37 +++++ src/blitsFile/virtualDocuments.js | 62 +++++++ src/commands/commentCommand.js | 142 ++++++++--------- src/extension.js | 50 +++--- src/fileManager.js | 138 ---------------- 13 files changed, 866 insertions(+), 235 deletions(-) create mode 100644 src/blitsFile/codeActionsProvider.js create mode 100644 src/blitsFile/completionProvider.js create mode 100644 src/blitsFile/config/compilerOptions.js create mode 100644 src/blitsFile/diagnostics.js create mode 100644 src/blitsFile/hoverProvider.js create mode 100644 src/blitsFile/languageService.js create mode 100644 src/blitsFile/signatureHelpProvider.js create mode 100644 src/blitsFile/utils/fileNameGenerator.js create mode 100644 src/blitsFile/utils/scriptExtractor.js create mode 100644 src/blitsFile/virtualDocuments.js delete mode 100644 src/fileManager.js diff --git a/src/blitsFile/codeActionsProvider.js b/src/blitsFile/codeActionsProvider.js new file mode 100644 index 0000000..dd03db8 --- /dev/null +++ b/src/blitsFile/codeActionsProvider.js @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const vscode = require('vscode') +const { getVirtualFileName } = require('./utils/fileNameGenerator') +const { getLanguageServiceInstance } = require('./languageService') +const { extractScriptContent } = require('./utils/scriptExtractor') + +function registerCodeActionsProvider(context) { + const codeActionProvider = vscode.languages.registerCodeActionsProvider( + 'blits', + { + provideCodeActions(document, range) { + const scriptInfo = extractScriptContent(document.getText()) + if (!scriptInfo) return + + const startOffset = document.offsetAt(range.start) - scriptInfo.startIndex + const endOffset = document.offsetAt(range.end) - scriptInfo.startIndex + if (startOffset < 0 || endOffset < 0) return + + const virtualFileName = getVirtualFileName(document.uri, scriptInfo.lang) + const languageService = getLanguageServiceInstance() + const diagnostics = languageService + .getSemanticDiagnostics(virtualFileName) + .filter((d) => d.start >= startOffset && d.start + d.length <= endOffset) + + const actions = diagnostics + .map((diagnostic) => { + if (diagnostic.code === 7027) { + // TS7027: Unreachable code detected + const fix = new vscode.CodeAction('Remove unreachable code', vscode.CodeActionKind.QuickFix) + const startPos = document.positionAt(scriptInfo.startIndex + diagnostic.start) + const endPos = document.positionAt(scriptInfo.startIndex + diagnostic.start + diagnostic.length) + fix.edit = new vscode.WorkspaceEdit() + fix.edit.delete(document.uri, new vscode.Range(startPos, endPos)) + return fix + } + return null + }) + .filter((action) => action !== null) + + return actions + }, + }, + { + providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], + } + ) + context.subscriptions.push(codeActionProvider) +} + +module.exports = { registerCodeActionsProvider } diff --git a/src/blitsFile/completionProvider.js b/src/blitsFile/completionProvider.js new file mode 100644 index 0000000..a73f0ab --- /dev/null +++ b/src/blitsFile/completionProvider.js @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const vscode = require('vscode') +const { getVirtualFileName } = require('./utils/fileNameGenerator') +const { getLanguageServiceInstance } = require('./languageService') +const { extractScriptContent } = require('./utils/scriptExtractor') + +function capitalizeFirstLetter(str) { + if (!str) return '' + return str.charAt(0).toUpperCase() + str.slice(1) +} + +function registerCompletionProvider(context) { + const completionProvider = vscode.languages.registerCompletionItemProvider( + 'blits', + { + provideCompletionItems(document, position) { + const scriptInfo = extractScriptContent(document.getText()) + if (!scriptInfo) return + + const offset = document.offsetAt(position) - scriptInfo.startIndex + if (offset < 0) return + + const virtualFileName = getVirtualFileName(document.uri, scriptInfo.lang) + const { getLanguageService } = getLanguageServiceInstance() + const languageService = getLanguageService(virtualFileName) + + if (!languageService) return + + const completions = languageService.getCompletionsAtPosition(virtualFileName, offset, { + includeCompletionsWithInsertText: true, + }) + + if (!completions) return + + return completions.entries.map((entry) => { + // Normalize the kind name to match VSCode's CompletionItemKind keys + const kindName = capitalizeFirstLetter(entry.kind.toLowerCase()) + const mappedKind = vscode.CompletionItemKind[kindName] + + /** + * @type {vscode.CompletionItemKind} + */ + const kind = typeof mappedKind === 'number' ? mappedKind : vscode.CompletionItemKind.Variable + + const item = new vscode.CompletionItem(entry.name, kind) + item.detail = entry.kind + if (entry.kindModifiers) { + item.detail += ` (${entry.kindModifiers})` + } + return item + }) + }, + }, + '.', // Trigger characters + '(', + '@' // Trigger characters + ) + context.subscriptions.push(completionProvider) +} + +module.exports = { registerCompletionProvider } diff --git a/src/blitsFile/config/compilerOptions.js b/src/blitsFile/config/compilerOptions.js new file mode 100644 index 0000000..37707d2 --- /dev/null +++ b/src/blitsFile/config/compilerOptions.js @@ -0,0 +1,51 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const ts = require('typescript') + +// Base compiler options applicable to both JS and TS +const baseCompilerOptions = { + target: ts.ScriptTarget.ES2020, + module: ts.ModuleKind.ES2020, + moduleResolution: ts.ModuleResolutionKind.NodeNext, + esModuleInterop: true, + noEmit: true, + baseUrl: '.', + paths: { + '*': ['node_modules/*'], + }, + include: ['blits.d.ts', '**/*.ts', '**/*.js', '**/*.blits'], + allowArbitraryExtensions: true, + allowNonTsExtensions: true, +} + +// JavaScript-specific compiler options +const jsCompilerOptions = { + ...baseCompilerOptions, + allowJs: true, + checkJs: true, +} + +// TypeScript-specific compiler options +const tsCompilerOptions = { + ...baseCompilerOptions, + noUnusedLocals: true, + noUnusedParameters: true, + noImplicitReturns: false, +} + +module.exports = { jsCompilerOptions, tsCompilerOptions, baseCompilerOptions } diff --git a/src/blitsFile/diagnostics.js b/src/blitsFile/diagnostics.js new file mode 100644 index 0000000..dc60a0d --- /dev/null +++ b/src/blitsFile/diagnostics.js @@ -0,0 +1,107 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const vscode = require('vscode') +const ts = require('typescript') +const { extractScriptContent } = require('./utils/scriptExtractor') +const { getVirtualFileName } = require('./utils/fileNameGenerator') +const { getLanguageServiceInstance } = require('./languageService') +const { addVirtualFile, deleteVirtualFilesByUri } = require('./virtualDocuments') + +const diagnosticCollection = vscode.languages.createDiagnosticCollection('blits') + +function registerDiagnostics(context) { + context.subscriptions.push(diagnosticCollection) + + const updateDiagnostics = (document) => { + const scriptInfo = extractScriptContent(document.getText()) + if (!scriptInfo) { + diagnosticCollection.set(document.uri, []) + return + } + + const virtualFileName = getVirtualFileName(document.uri, scriptInfo.lang) + addVirtualFile(virtualFileName, scriptInfo.content, document.version) + + const { getLanguageService } = getLanguageServiceInstance() + const languageService = getLanguageService(virtualFileName) + + if (!languageService) { + diagnosticCollection.set(document.uri, []) + return + } + + const syntacticDiagnostics = languageService.getSyntacticDiagnostics(virtualFileName) + const semanticDiagnostics = languageService.getSemanticDiagnostics(virtualFileName) + const allDiagnostics = syntacticDiagnostics.concat(semanticDiagnostics) + + const diagnostics = allDiagnostics + .map((diagnostic) => { + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') + if (diagnostic.file) { + const start = scriptInfo.startIndex + diagnostic.start + const end = + diagnostic.start !== undefined && diagnostic.length !== undefined ? start + diagnostic.length : start + 1 // Fallback if length is undefined + + const startPos = document.positionAt(start) + const endPos = document.positionAt(end) + + return new vscode.Diagnostic( + new vscode.Range(startPos, endPos), + message, + diagnostic.category === ts.DiagnosticCategory.Error + ? vscode.DiagnosticSeverity.Error + : vscode.DiagnosticSeverity.Warning + ) + } + return null + }) + .filter((d) => d !== null) + + diagnosticCollection.set(document.uri, diagnostics) + } + + // Event listeners + context.subscriptions.push( + vscode.workspace.onDidChangeTextDocument((event) => { + if (event.document.languageId === 'blits') { + updateDiagnostics(event.document) + } + }), + vscode.workspace.onDidOpenTextDocument((document) => { + if (document.languageId === 'blits') { + updateDiagnostics(document) + } + }), + vscode.workspace.onDidCloseTextDocument((document) => { + if (document.languageId === 'blits') { + deleteVirtualFilesByUri(document.uri) + diagnosticCollection.delete(document.uri) + } + }) + ) + + // Initialize diagnostics for active editor + if (vscode.window.activeTextEditor) { + const document = vscode.window.activeTextEditor.document + if (document.languageId === 'blits') { + updateDiagnostics(document) + } + } +} + +module.exports = { registerDiagnostics } diff --git a/src/blitsFile/hoverProvider.js b/src/blitsFile/hoverProvider.js new file mode 100644 index 0000000..cdd4253 --- /dev/null +++ b/src/blitsFile/hoverProvider.js @@ -0,0 +1,53 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const vscode = require('vscode') +const { getVirtualFileName } = require('./utils/fileNameGenerator') +const { getLanguageServiceInstance } = require('./languageService') +const { extractScriptContent } = require('./utils/scriptExtractor') + +function registerHoverProvider(context) { + const hoverProvider = vscode.languages.registerHoverProvider('blits', { + provideHover(document, position) { + const scriptInfo = extractScriptContent(document.getText()) + if (!scriptInfo) return + + const offset = document.offsetAt(position) - scriptInfo.startIndex + if (offset < 0) return + + const virtualFileName = getVirtualFileName(document.uri, scriptInfo.lang) + const { getLanguageService } = getLanguageServiceInstance() + const languageService = getLanguageService(virtualFileName) + + if (!languageService) return + + const hoverInfo = languageService.getQuickInfoAtPosition(virtualFileName, offset) + + if (hoverInfo && hoverInfo.displayParts) { + const contents = hoverInfo.displayParts.map((part) => part.text).join('') + const markdown = new vscode.MarkdownString() + markdown.appendCodeblock(contents, scriptInfo.lang === 'ts' ? 'typescript' : 'javascript') + + const range = document.getWordRangeAtPosition(position) + return new vscode.Hover(markdown, range) + } + }, + }) + context.subscriptions.push(hoverProvider) +} + +module.exports = { registerHoverProvider } diff --git a/src/blitsFile/languageService.js b/src/blitsFile/languageService.js new file mode 100644 index 0000000..c570598 --- /dev/null +++ b/src/blitsFile/languageService.js @@ -0,0 +1,195 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const ts = require('typescript') +const fs = require('fs') +const path = require('path') +const vscode = require('vscode') +const { getAllVirtualFiles, addVirtualFile, getVirtualFile } = require('./virtualDocuments') +const { jsCompilerOptions, tsCompilerOptions } = require('./config/compilerOptions') +const { extractScriptContent } = require('./utils/scriptExtractor') +const { getVirtualFileName } = require('./utils/fileNameGenerator') + +// Language Service Instances (Initially null for lazy-loading) +let jsLanguageService = null +let tsLanguageService = null + +// Language Service Hosts (Factory Function) +function createLanguageServiceHost(compilerOptions) { + return { + getScriptFileNames: () => { + const scriptFiles = vscode.workspace.textDocuments + .filter((doc) => ['javascript', 'typescript', 'blits'].includes(doc.languageId)) + .map((doc) => doc.uri.fsPath) + const blitsFiles = Array.from(getAllVirtualFiles().keys()) + return [...scriptFiles, ...blitsFiles] + }, + getScriptVersion: (fileName) => { + const virtualFile = getVirtualFile(fileName) + if (virtualFile) { + return virtualFile.version.toString() + } + const doc = vscode.workspace.textDocuments.find((doc) => doc.uri.fsPath === fileName) + return doc ? doc.version.toString() : '1' + }, + getScriptSnapshot: (fileName) => { + let content + + if (path.extname(fileName) === '.blits') { + const { virtualFile, lang } = getOrCreateVirtualDocument(fileName) || {} + if (virtualFile) { + content = virtualFile.content + } + } else { + const virtualFile = getVirtualFile(fileName) + if (virtualFile) { + content = virtualFile.content + } else if (fs.existsSync(fileName)) { + content = fs.readFileSync(fileName, 'utf8') + } + } + + return content ? ts.ScriptSnapshot.fromString(content) : undefined + }, + getCurrentDirectory: () => { + const workspaceFolders = vscode.workspace.workspaceFolders + return workspaceFolders && workspaceFolders.length > 0 ? workspaceFolders[0].uri.fsPath : process.cwd() + }, + getCompilationSettings: () => compilerOptions, + getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options), + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + readDirectory: ts.sys.readDirectory, + directoryExists: ts.sys.directoryExists, + getDirectories: ts.sys.getDirectories, + resolveModuleNames: (moduleNames, containingFile) => { + const containingLang = path.extname(containingFile) === '.ts' ? 'ts' : 'js' + const currentCompilerOptions = containingLang === 'ts' ? tsCompilerOptions : jsCompilerOptions + + return moduleNames.map((moduleName) => { + const result = ts.resolveModuleName(moduleName, containingFile, currentCompilerOptions, { + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + }) + + if (result.resolvedModule) { + return result.resolvedModule + } + + const blitsExtension = '.blits' + if (moduleName.endsWith(blitsExtension)) { + const fullPath = path.resolve(path.dirname(containingFile), moduleName) + if (ts.sys.fileExists(fullPath)) { + const { lang } = getOrCreateVirtualDocument(fullPath) || {} + if (lang) { + const virtualFileName = getVirtualFileName(vscode.Uri.file(fullPath), lang) + return { resolvedFileName: virtualFileName, extension: `.${lang}`, isExternalLibraryImport: false } + } + return { resolvedFileName: fullPath, extension: blitsExtension, isExternalLibraryImport: false } + } + } + + const blitsPath = path.resolve(path.dirname(containingFile), moduleName + blitsExtension) + if (ts.sys.fileExists(blitsPath)) { + const { lang } = getOrCreateVirtualDocument(blitsPath) || {} + if (lang) { + const virtualFileName = getVirtualFileName(vscode.Uri.file(blitsPath), lang) + return { resolvedFileName: virtualFileName, extension: `.${lang}`, isExternalLibraryImport: false } + } + return { resolvedFileName: blitsPath, extension: blitsExtension, isExternalLibraryImport: false } + } + + return undefined + }) + }, + } +} + +function initializeJsLanguageService() { + if (!jsLanguageService) { + const jsLanguageServiceHost = createLanguageServiceHost(jsCompilerOptions) + jsLanguageService = ts.createLanguageService(jsLanguageServiceHost, ts.createDocumentRegistry()) + console.log('JavaScript Language Service initialized.') + } + return jsLanguageService +} + +function initializeTsLanguageService() { + if (!tsLanguageService) { + const tsLanguageServiceHost = createLanguageServiceHost(tsCompilerOptions) + tsLanguageService = ts.createLanguageService(tsLanguageServiceHost, ts.createDocumentRegistry()) + console.log('TypeScript Language Service initialized.') + } + return tsLanguageService +} + +function getLanguageService(fileName) { + const ext = path.extname(fileName) + if (ext === '.ts' || ext === '.tsx') { + return initializeTsLanguageService() + } else if (ext === '.js' || ext === '.jsx') { + return initializeJsLanguageService() + } + // For .blits files, determine language from 'lang' attribute + if (ext === '.blits') { + const { lang } = getOrCreateVirtualDocument(fileName) || {} + return lang === 'ts' ? initializeTsLanguageService() : initializeJsLanguageService() + } + return null // No Language Service for unsupported file types +} + +function getOrCreateVirtualDocument(fileName) { + const uri = vscode.Uri.file(fileName) + const content = fs.readFileSync(fileName, 'utf8') + const scriptInfo = extractScriptContent(content) + + if (!scriptInfo) return null + + const { lang, content: scriptContent } = scriptInfo + const virtualFileName = getVirtualFileName(uri, lang) + let virtualFile = getVirtualFile(virtualFileName) + + if (!virtualFile) { + addVirtualFile(virtualFileName, scriptContent, 1) + virtualFile = { content: scriptContent, version: 1 } + } + + return { virtualFile, lang } +} + +function disposeLanguageServices() { + if (jsLanguageService) { + jsLanguageService.dispose() + jsLanguageService = null + console.log('JavaScript Language Service disposed.') + } + if (tsLanguageService) { + tsLanguageService.dispose() + tsLanguageService = null + console.log('TypeScript Language Service disposed.') + } +} + +// Exported function to get Language Services and manage lifecycle +function getLanguageServiceInstance() { + return { + getLanguageService, + disposeLanguageServices, + } +} + +module.exports = { getLanguageServiceInstance } diff --git a/src/blitsFile/signatureHelpProvider.js b/src/blitsFile/signatureHelpProvider.js new file mode 100644 index 0000000..a10e2d1 --- /dev/null +++ b/src/blitsFile/signatureHelpProvider.js @@ -0,0 +1,80 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const vscode = require('vscode') +const { getVirtualFileName } = require('./utils/fileNameGenerator') +const { getLanguageServiceInstance } = require('./languageService') +const { extractScriptContent } = require('./utils/scriptExtractor') + +function registerSignatureHelpProvider(context) { + const signatureHelpProvider = vscode.languages.registerSignatureHelpProvider( + 'blits', + { + provideSignatureHelp(document, position) { + const scriptInfo = extractScriptContent(document.getText()) + if (!scriptInfo) return + + const offset = document.offsetAt(position) - scriptInfo.startIndex + if (offset < 0) return + + const virtualFileName = getVirtualFileName(document.uri, scriptInfo.lang) + const { getLanguageService } = getLanguageServiceInstance() + const languageService = getLanguageService(virtualFileName) + + if (!languageService) return + + const sigHelp = languageService.getSignatureHelpItems(virtualFileName, offset, {}) + + if (!sigHelp) return + + const signatures = sigHelp.items.map((item) => { + const label = + item.prefixDisplayParts.map((part) => part.text).join('') + + item.parameters.map((param) => param.displayParts.map((part) => part.text).join('')).join(', ') + + item.suffixDisplayParts.map((part) => part.text).join('') + + const documentation = item.documentation?.map((part) => part.text).join('') || '' + + const signatureInfo = new vscode.SignatureInformation(label, new vscode.MarkdownString(documentation)) + + signatureInfo.parameters = item.parameters.map((param) => { + return new vscode.ParameterInformation( + param.displayParts.map((part) => part.text).join(''), + param.documentation + ? new vscode.MarkdownString(param.documentation.map((part) => part.text).join('')) + : undefined + ) + }) + + return signatureInfo + }) + + const signatureHelp = new vscode.SignatureHelp() + signatureHelp.signatures = signatures + signatureHelp.activeSignature = sigHelp.selectedItemIndex + signatureHelp.activeParameter = sigHelp.argumentIndex + + return signatureHelp + }, + }, + '(', // Trigger characters + ',' // Trigger characters + ) + context.subscriptions.push(signatureHelpProvider) +} + +module.exports = { registerSignatureHelpProvider } diff --git a/src/blitsFile/utils/fileNameGenerator.js b/src/blitsFile/utils/fileNameGenerator.js new file mode 100644 index 0000000..9d6c110 --- /dev/null +++ b/src/blitsFile/utils/fileNameGenerator.js @@ -0,0 +1,43 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const path = require('path') + +function getVirtualFileName(uri, lang) { + const extension = lang === 'ts' ? 'ts' : 'js' + let normalizedPath = uri.fsPath + + // Normalize the path (handles both forward and backslashes) + normalizedPath = path.normalize(normalizedPath) + + // Replace .blits with .ts or .js + if (normalizedPath.endsWith('.blits')) { + normalizedPath = normalizedPath.slice(0, -6) // Remove '.blits' + } + + // Ensure the path is absolute + normalizedPath = path.resolve(normalizedPath) + + // Handle UNC paths + if (normalizedPath.startsWith('\\\\')) { + normalizedPath = normalizedPath.replace(/\\/g, '/') + } + + return `${normalizedPath}.blits.${extension}` +} + +module.exports = { getVirtualFileName } diff --git a/src/blitsFile/utils/scriptExtractor.js b/src/blitsFile/utils/scriptExtractor.js new file mode 100644 index 0000000..4a13921 --- /dev/null +++ b/src/blitsFile/utils/scriptExtractor.js @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +function extractScriptContent(text) { + const scriptTagRegex = /([\s\S]*?)<\/script>/ + const match = scriptTagRegex.exec(text) + + if (!match) return null + + const lang = match[1] || 'js' + const scriptContent = match[2] + const scriptStartIndex = match.index + match[0].indexOf('>') + 1 + + return { + lang, + content: scriptContent, + startIndex: scriptStartIndex, + } +} + +module.exports = { + extractScriptContent, +} diff --git a/src/blitsFile/virtualDocuments.js b/src/blitsFile/virtualDocuments.js new file mode 100644 index 0000000..6128c40 --- /dev/null +++ b/src/blitsFile/virtualDocuments.js @@ -0,0 +1,62 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const path = require('path') +const os = require('os') + +const virtualFiles = new Map() +const isWindows = os.platform() === 'win32' + +function normalizeFilePath(filePath) { + return path.normalize(filePath) +} + +function getFileKey(filePath) { + const normalized = normalizeFilePath(filePath) + return isWindows ? normalized.toLowerCase() : normalized +} + +function addVirtualFile(fileName, content, version) { + const key = getFileKey(fileName) + virtualFiles.set(key, { fileName, content, version }) +} + +function getVirtualFile(fileName) { + const key = getFileKey(fileName) + const file = virtualFiles.get(key) + return file ? { ...file, fileName: file.fileName } : undefined +} + +function deleteVirtualFilesByUri(uri) { + const normalizedUriPath = getFileKey(uri.fsPath) + Array.from(virtualFiles.keys()).forEach((key) => { + if (getFileKey(key).startsWith(normalizedUriPath)) { + virtualFiles.delete(key) + } + }) +} + +function getAllVirtualFiles() { + return new Map(Array.from(virtualFiles.entries()).map(([key, value]) => [value.fileName, value])) +} + +module.exports = { + addVirtualFile, + getVirtualFile, + deleteVirtualFilesByUri, + getAllVirtualFiles, +} diff --git a/src/commands/commentCommand.js b/src/commands/commentCommand.js index b2ea1b6..a36a6e3 100644 --- a/src/commands/commentCommand.js +++ b/src/commands/commentCommand.js @@ -18,101 +18,89 @@ const vscode = require('vscode') const templateHelper = require('../helpers/template') const parse = require('../parsers') -const { is } = require('@babel/traverse/lib/path/introspection') -module.exports = vscode.commands.registerCommand( - 'blits-vscode.commentCommand', - async () => { - const editor = vscode.window.activeTextEditor +module.exports = vscode.commands.registerCommand('blits-vscode.commentCommand', async () => { + const editor = vscode.window.activeTextEditor - if (editor) { - const document = editor.document - const selection = editor.selection - const isBlits = document.languageId === 'blits' + if (editor) { + const document = editor.document + const selection = editor.selection + const isBlits = document.languageId === 'blits' - const currentDoc = document.getText() - const cursorPosition = selection.active + const currentDoc = document.getText() + const cursorPosition = selection.active - let isCursorInsideTemplate = false - if (isBlits) { - isCursorInsideTemplate = templateHelper.isCursorInsideTemplateForBlits( - document, - currentDoc, - cursorPosition - ) - } else { - const currentDocAst = parse.AST(currentDoc) - isCursorInsideTemplate = templateHelper.isCursorInsideTemplate( - document, - currentDocAst, - cursorPosition - ) - } + let isCursorInsideTemplate = false + if (isBlits) { + isCursorInsideTemplate = templateHelper.isCursorInsideTemplateForBlits(document, currentDoc, cursorPosition) + } else { + const currentDocAst = parse.AST(currentDoc) + isCursorInsideTemplate = templateHelper.isCursorInsideTemplate(document, currentDocAst, cursorPosition) + } - if (isCursorInsideTemplate) { - await editor.edit((editBuilder) => { - let startLine, endLine + if (isCursorInsideTemplate) { + await editor.edit((editBuilder) => { + let startLine, endLine - if (selection.isEmpty) { - // If no text is selected, get the entire line where the cursor is - startLine = endLine = selection.start.line - } else { - // Get the entire lines that the selection spans - startLine = selection.start.line - endLine = selection.end.line - } + if (selection.isEmpty) { + // If no text is selected, get the entire line where the cursor is + startLine = endLine = selection.start.line + } else { + // Get the entire lines that the selection spans + startLine = selection.start.line + endLine = selection.end.line + } - const isBlockSelection = endLine > startLine + const isBlockSelection = endLine > startLine - for (let i = startLine; i <= endLine; i++) { - const line = document.lineAt(i) - let lineText = line.text.replace(/^\s*/, '') - let selectionRange = line.range - const leadingWhitespace = line.text.match(/^\s*/)[0] + for (let i = startLine; i <= endLine; i++) { + const line = document.lineAt(i) + let lineText = line.text.replace(/^\s*/, '') + let selectionRange = line.range + const leadingWhitespace = line.text.match(/^\s*/)[0] - // Check if the line is already an HTML comment - if (isLineCommented(lineText)) { - // Remove the comment from single line comment - if (!isBlockSelection) { - lineText = line.text.replace(/^(\s*)/g, '$1$2') - } else { - if (i === startLine) { - lineText = line.text.replace(/^(\s*)/g, '$1') - } - } + // Check if the line is already an HTML comment + if (isLineCommented(lineText)) { + // Remove the comment from single line comment + if (!isBlockSelection) { + lineText = line.text.replace(/^(\s*)/g, '$1$2') } else { - // Add a comment - if (isLineTemplateStart(lineText)) { - lineText = document.getText(selection) - selectionRange = selection + if (i === startLine) { + lineText = line.text.replace(/^(\s*)/g, '$1') } + } + } else { + // Add a comment + if (isLineTemplateStart(lineText)) { + lineText = document.getText(selection) + selectionRange = selection + } - if (isBlockSelection) { - if (i === startLine) { - lineText = `${leadingWhitespace}` - } else { - lineText = line.text - } + if (isBlockSelection) { + if (i === startLine) { + lineText = `${leadingWhitespace}` } else { - lineText = `${leadingWhitespace}` + lineText = line.text } + } else { + lineText = `${leadingWhitespace}` } - - // Replace the line in the editor - editBuilder.replace(selectionRange, lineText) } - }) - } else { - // Otherwise, execute the built-in comment command - await vscode.commands.executeCommand('editor.action.commentLine') - } + + // Replace the line in the editor + editBuilder.replace(selectionRange, lineText) + } + }) + } else { + // Otherwise, execute the built-in comment command + await vscode.commands.executeCommand('editor.action.commentLine') } } -) +}) const isLineCommented = (lineText) => { return lineText.match(/|/) diff --git a/src/extension.js b/src/extension.js index b76bfc6..c038727 100644 --- a/src/extension.js +++ b/src/extension.js @@ -15,34 +15,39 @@ * SPDX-License-Identifier: Apache-2.0 */ -const fileManager = require('./fileManager') +// const fileManager = require('./fileManager') const completionProviders = require('./completionProviders') const commands = require('./commands') const formatters = require('./formatters') const completionItems = require('./completionItems') const errorChecking = require('./errorChecking') const vscode = require('vscode') +const { registerDiagnostics } = require('./blitsFile/diagnostics') +const { registerHoverProvider } = require('./blitsFile/hoverProvider') +const { registerCompletionProvider } = require('./blitsFile/completionProvider') +const { registerSignatureHelpProvider } = require('./blitsFile/signatureHelpProvider') +// const { registerCodeActionsProvider } = require('./blitsFile/codeActionsProvider') // not working yet +const { getLanguageServiceInstance } = require('./blitsFile/languageService') +const packageJSON = require('../package.json') async function activate(context) { - // initiate file manager - // fileManager.init(context) + console.log('Lightning Blits is being activated.') - // let disposable = vscode.workspace.onDidChangeTextDocument((event) => { - // event.contentChanges.forEach((change) => { - // console.log( - // `Change at range: ${change.range.start.line}:${change.range.start.character} - ${change.range.end.line}:${change.range.end.character}` - // ) - // console.log( - // `Replaced ${change.rangeLength} characters with '${change.text}'` - // ) - // }) - // }) + try { + // Blits file type features + const languageService = getLanguageServiceInstance() + if (!languageService) { + throw new Error('Failed to initialize TypeScript language service') + } - // context.subscriptions.push(disposable) + registerDiagnostics(context) + registerHoverProvider(context) + registerCompletionProvider(context) + registerSignatureHelpProvider(context) + // registerCodeActionsProvider(context) - console.log('Lightning Blits is being activated.') + // Other features - try { // get element/renderer props from Blits codebase console.log('Parsing element props from Blits codebase') const isElementPropsReady = await completionItems.elementProps.parseProps() @@ -59,19 +64,24 @@ async function activate(context) { context.subscriptions.push(formatters.templateFormatterOnSave) // create diagnostic collection for error checking - const diagnosticsCollection = - vscode.languages.createDiagnosticCollection('blits') + const diagnosticsCollection = vscode.languages.createDiagnosticCollection('blits-template') errorChecking.checkForLoopIndexAsKey(context, diagnosticsCollection) + // extension activated + vscode.window.showInformationMessage(`Lightning Blits v${packageJSON.version} has been activated!`) console.log('Lightning Blits has been activated.') } catch (error) { console.error('Error activating Lightning Blits:', error) + vscode.window.showErrorMessage(`Failed to activate Lightning Blits v${packageJSON.version}: ${error.message}`) } } function deactivate() { - // fileManager.clearAllFiles() - console.log('Lightning Blits has been deactivated.') + console.log('Lightning Blits is being deactivated.') + const languageServiceInstance = getLanguageServiceInstance() + if (languageServiceInstance && typeof languageServiceInstance.disposeLanguageServices === 'function') { + languageServiceInstance.disposeLanguageServices() + } } module.exports = { diff --git a/src/fileManager.js b/src/fileManager.js deleted file mode 100644 index b68bf32..0000000 --- a/src/fileManager.js +++ /dev/null @@ -1,138 +0,0 @@ -// const templateHelper = require('./helpers/template') -const vscode = require('vscode') -const parse = require('./parsers') -const template = require('./helpers/template') - -let data = new Map() -let currentFile = '' -let keyPressTimer -const allowedFileSchemes = ['file', 'untitled'] - -const updateFiles = (doc, changes) => {} - -const updateFileSync = (doc) => { - // parse file - const fileExtension = uri.fsPath.split('.').pop() - const AST = parse.AST(content, fileExtension) - const templates = template.getTemplates(AST, content) - const hasTemplate = templates.length > 0 - - const fileData = { - content, - cursorPosition, - fileExtension, - AST, - hasTemplate, - templates, - isActive, - isParsed: true, - lastParsed: Date.now(), - } - data.set(uri.fsPath, fileData) - if (isActive) { - currentFile = uri.fsPath - } - console.log( - 'fileData', - fileData.isActive, - fileData.isParsed, - fileData.lastParsed - ) -} - -const getFile = (uri) => { - return data.get(uri.fsPath) -} - -const setActiveFile = (uri) => { - currentFile = uri.fsPath -} - -const getActiveFile = () => { - return data.get(currentFile) -} - -const clearFile = (uri) => { - data.delete(uri.fsPath) -} - -const updateFileAsync = (doc, cursorPosition, lastChar) => { - clearTimeout(keyPressTimer) - const delay = lastChar === '.' ? 0 : 300 // No delay if last character is a dot - keyPressTimer = setTimeout(() => { - updateFileSync(doc, cursorPosition) - }, delay) -} - -const clearAllFiles = () => { - data.clear() -} - -const getCursorPosition = (doc) => { - const activeEditor = vscode.window.activeTextEditor - if (activeEditor && activeEditor.document === doc) { - const position = activeEditor.selection.active - return position - } - return null -} - -const init = (context) => { - data = new Map() - - const openedDocEvent = vscode.workspace.onDidOpenTextDocument((doc) => { - updateFiles(doc) - }) - - const changedDocEvent = vscode.workspace.onDidChangeTextDocument((event) => { - updateFiles(event.document, event.contentChanges) - }) - - const savedDocEvent = vscode.workspace.onDidSaveTextDocument((doc) => { - console.log('savedDocEvent', doc.uri.fsPath) - }) - - const closedDocEvent = vscode.workspace.onDidCloseTextDocument((doc) => { - console.log('closedDocEvent', doc.uri.fsPath) - if (allowedFileSchemes.includes(doc.uri.scheme)) { - clearFile(doc) - } - }) - - const activeEditorChange = vscode.window.onDidChangeActiveTextEditor( - (editor) => { - if (editor && allowedFileSchemes.includes(editor.document.uri.scheme)) { - console.log('activeEditorChange', editor.document.uri.fsPath) - setActiveFile(editor.document.uri) - } - } - ) - - // event subscriptions - context.subscriptions.push(openedDocEvent) - context.subscriptions.push(changedDocEvent) - context.subscriptions.push(savedDocEvent) - context.subscriptions.push(closedDocEvent) - context.subscriptions.push(activeEditorChange) - - // get the active editor - const activeEditor = vscode.window.activeTextEditor - if ( - activeEditor && - allowedFileSchemes.includes(activeEditor.document.uri.scheme) - ) { - console.log('initialActiveEditor', activeEditor.document.uri.fsPath) - setActiveFile(activeEditor.document.uri) - updateFiles(activeEditor.document) - } -} - -module.exports = { - updateFileAsync, - getFile, - clearFile, - clearAllFiles, - updateFileSync, - init, - getActiveFile, -}