diff --git a/package.json b/package.json index 1164f55..859b399 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,17 @@ { "label": "Pine-Light #01" , "id": "Light Theme (#1)" , "uiTheme": "vs" , "path": "themes/Pine-Light#01.json" }, { "label": "Pine-Light #02" , "id": "Light Theme (#2)" , "uiTheme": "vs" , "path": "themes/Pine-Light#02.json" } ], "configuration": {}, - "submenus": [ { "id": "pine.mysubmenuNonPineFile", "label": " Pine Script v5" }, { "id": "pine.mysubmenu2", "label": " Pine Script v5" } ], + "submenus": [ { "id": "pine.mysubmenuNonPineFile", "label": " Pine Script v5" } ], + "colors": [ + { "id": "pine.errorBackground", "description": "Background color for error highlights.", "defaults": { "dark": "rgba(255, 0, 0, 0.1)", "light": "rgba(255, 0, 0, 0.1)", "highContrast": "rgba(255, 0, 0, 0.1)" } }, + { "id": "pine.warningBackground", "description": "Background color for warning highlights.", "defaults": { "dark": "rgba(255, 255, 0, 0.1)", "light": "rgba(255, 255, 0, 0.1)", "highContrast": "rgba(255, 255, 0, 0.1)" } }, + { "id": "pine.errorGutterIcon", "description": "Color for the error gutter icon. (Note: Current SVG icon has fixed color)", "defaults": { "dark": "editorOverviewRuler.errorForeground", "light": "editorOverviewRuler.errorForeground", "highContrast": "editorOverviewRuler.errorForeground" } }, + { + "id": "pine.warningGutterIcon", + "description": "Color for the warning gutter icon. (Note: Current SVG icon has fixed color)", + "defaults": { "dark": "editorOverviewRuler.warningForeground", "light": "editorOverviewRuler.warningForeground", "highContrast": "editorOverviewRuler.warningForeground" } + } + ], "commands": [ { "command": "pine.mysubmenuNonPineFile", "title": "Pine Script Options" , "category": "navigation", "when": "!editorLangId == pine" }, { "command": "pine.mysubmenu2" , "title": "Pine Script Options" , "category": "navigation", "when": "editorLangId == pine" }, { "command": "pine.typify" , "title": "Typify Variables" , "category": "navigation" }, { "command": "pine.completionAccepted" , "title": "Completion Accepted" }, @@ -48,19 +58,18 @@ ], "menus": { "editor/context": [ - { "submenu": "pine.mysubmenuNonPineFile", "when": "editorLangId !== pine" , "group": "Pine" }, - { "submenu": "pine.mysubmenu2" , "when": "editorLangId == pine" , "group": "1Pine" }, - { "when": "editorLangId == pine && editorHasSelection", "group": "1Pine", "command": "pine.typify" }, - { "when": "editorLangId == pine && editorHasSelection", "group": "1Pine", "command": "pine.docString" }, - { "when": "editorLangId == pine" , "group": "1Pine", "command": "pine.getStandardList" } + { "submenu": "pine.mysubmenuNonPineFile", "when": "editorLangId !== pine" , "group": "Pine" }, + { "when": "editorLangId == pine && editorHasSelection", "group": "9_pinescript@1", "command": "pine.typify" }, + { "when": "editorLangId == pine && editorHasSelection", "group": "9_pinescript@2", "command": "pine.docString" }, + { "when": "editorLangId == pine" , "group": "9_pinescript@3", "command": "pine.getStandardList" } ], - "pine.mysubmenuNonPineFile": [ { "command": "pine.getStandardList" }, { "command": "pine.getIndicatorTemplate" }, { "command": "pine.getStrategyTemplate" }, { "command": "pine.getLibraryTemplate" } ], - "pine.mysubmenu2": [ { "command": "pine.docString" }, { "command": "pine.setUsername" }, { "command": "pine.typify" }, { "command": "pine.getIndicatorTemplate" }, { "command": "pine.getStrategyTemplate" }, { "command": "pine.getLibraryTemplate" } ] + "pine.mysubmenuNonPineFile": [ { "command": "pine.getStandardList" }, { "command": "pine.getIndicatorTemplate" }, { "command": "pine.getStrategyTemplate" }, { "command": "pine.getLibraryTemplate" } ] }, "snippets": [], "languages": [ { "id": "pine", "icon": { "light": "media/PineLogo.png", "dark": "media/PineLogo.png" }, "aliases": [ "pinescript", "pine" ], "extensions": [ ".ps", ".pine", ".pinescript" ], "configuration": "config/language-configuration.json" } ], "documentSymbolProvider": [ { "language": "pine", "scheme": "file" } ], - "grammars": [ { "language": "pine", "scopeName": "source.pine", "path": "syntaxes/pine.tmLanguage.json" }, { "scopeName": "source.markdown", "path": "syntaxes/pine-embedded.tmLanguage.json", "injectTo": [ "text.html.markdown" ] } ] + "grammars": [ { "language": "pine", "scopeName": "source.pine", "path": "syntaxes/pine.tmLanguage.json" }, { "scopeName": "source.markdown", "path": "syntaxes/pine-embedded.tmLanguage.json", "injectTo": [ "text.html.markdown" ] } ], + "configurationDefaults": { "[pine]": { "errorLens.enabled": true, "errorLens.enabledDiagnosticLevels": [ "error", "warning", "info" ], "errorLens.messageBackgroundMode": "line", "errorLens.messageEnabled": true, "errorLens.gutterIconsEnabled": true, "errorLens.gutterIconSet": "codicons" } } }, "dependencies": { "debounce": "^2.1.1", "lodash": "^4.17.21", "node-fetch": "^3.3.2" }, "devDependencies": { @@ -89,5 +98,6 @@ "webpack" : "^5.94.0", "webpack-cli" : "^5.1.4" , "xo" : "^0.54.0" - } + }, + "extensionDependencies": [ "usernamehw.errorlens" ] } diff --git a/src/PineClass.ts b/src/PineClass.ts index 203e555..bd84179 100644 --- a/src/PineClass.ts +++ b/src/PineClass.ts @@ -7,7 +7,9 @@ import { PineUserInputs } from './PineUserInputs' import { PineHoverProvider } from './PineHoverProvider/PineHoverProvider' import { PineLibCompletionProvider } from './PineLibCompletionProvider' import { PineLibHoverProvider } from './PineLibHoverProvider' -import { PineInlineCompletionContext, PineCompletionProvider } from './PineCompletionProvider' +import { PineCompletionProvider } from './PineCompletionProvider' +import { PineInlineCompletionContext } from './PineInlineCompletionContext' +// * Lints the current PineScript file. import { PineFormatResponse } from './PineFormatResponse' import { PineScriptList } from './PineScriptList' import { PineTemplates } from './PineTemplates' @@ -17,11 +19,13 @@ import { PineHoverFunction } from './PineHoverProvider/PineHoverIsFunction' import { PineHoverMethod } from './PineHoverProvider/PineHoverIsMethod' import { PineRenameProvider } from './PineRenameProvider' import { PineParser } from './PineParser' +import { PineCompletionService } from './PineCompletionService' export class Class { public static context: vscode.ExtensionContext | undefined public static pineDocsManager: PineDocsManager + public static pineCompletionService: PineCompletionService; public static pineUserInputs: PineUserInputs public static pineRequest: PineRequest public static pineHoverProvider: PineHoverProvider diff --git a/src/PineCompletionProvider.ts b/src/PineCompletionProvider.ts index fe5a713..7d705d3 100644 --- a/src/PineCompletionProvider.ts +++ b/src/PineCompletionProvider.ts @@ -1,8 +1,10 @@ import { Helpers, PineSharedCompletionState } from './index' import { Class } from './PineClass' import * as vscode from 'vscode' +import { CompletionDoc } from './PineCompletionService' // PineCompletionService itself is no longer instantiated here export class PineCompletionProvider implements vscode.CompletionItemProvider { + // private pineCompletionService: PineCompletionService // Removed completionItems: vscode.CompletionItem[] = [] docType: any userDocs: any @@ -14,6 +16,13 @@ export class PineCompletionProvider implements vscode.CompletionItemProvider { argumentCompletionsFlag: boolean = false sigCompletions: Record = {} + constructor() { + // Constructor is now empty or can be used for other initializations + // if (!Class.pineCompletionService) { + // console.error("PineCompletionService not initialized in Class object!"); + // } + } + /** * Checks if completions are available for the current context. * @returns An array of completions. @@ -78,19 +87,21 @@ export class PineCompletionProvider implements vscode.CompletionItemProvider { document: vscode.TextDocument, name: string, namespace: string | null, - doc: any, + doc: CompletionDoc, // Changed from any to CompletionDoc position: vscode.Position, argCompletion: boolean = false, ) { try { // Determine if the item is a method - const isMethod = doc?.isMethod ?? false + const isMethod = doc.isMethod ?? false // Get the kind of the item - const kind = doc?.kind + const kind = doc.kind // Determine if the item is the default (applies to function params for now) - let preselect = doc?.preselect ?? false ? doc.preselect : doc?.default ?? false + // doc.doc refers to the original documentation object stored inside CompletionDoc + let preselect = doc.doc?.preselect ?? doc.default ?? false // name the label variable + // 'name' parameter is the specific name for this completion (e.g. function name, method name) let label = name // If the item is a function or method, add parentheses to the name let openParen = '' @@ -103,14 +114,17 @@ export class PineCompletionProvider implements vscode.CompletionItemProvider { moveCursor = true } // Format the syntax and check for overloads - const modifiedSyntax = Helpers.formatSyntax(name, doc, isMethod, namespace) + // Pass doc.doc (original doc object) to formatSyntax if it expects that structure + const modifiedSyntax = Helpers.formatSyntax(name, doc.doc, isMethod, namespace) // Format the label and description label = isMethod ? `${namespace}.${label.split('.').pop()}` : label label = label + openParen + closeParen - const formattedDesc = Helpers.formatUrl(Helpers?.checkDesc(doc?.desc)) + // Use doc.description from CompletionDoc, fallback to doc.doc.desc if needed by checkDesc + const formattedDesc = Helpers.formatUrl(Helpers?.checkDesc(doc.description || doc.doc?.desc)) // Determine the kind of the completion item - const itemKind = await this.determineCompletionItemKind(kind) + // Pass doc.doc (original doc object) to determineCompletionItemKind for docDetails + const itemKind = await this.determineCompletionItemKind(kind, doc.doc) // Create a new CompletionItem object const completionItem = new vscode.CompletionItem(label, itemKind) completionItem.documentation = new vscode.MarkdownString(`${formattedDesc} \`\`\`pine\n${modifiedSyntax}\n\`\`\``) @@ -165,15 +179,26 @@ export class PineCompletionProvider implements vscode.CompletionItemProvider { * @param kind - The type of the item. * @returns The kind of the completion item. */ - async determineCompletionItemKind(kind?: string) { + async determineCompletionItemKind(kind?: string, docDetails?: any) { + // Added docDetails parameter try { // If the kind is not specified, return Text as the default kind if (!kind) { return vscode.CompletionItemKind.Text } + // Check for const fields first (potential enum members) based on docDetails + if (docDetails?.isConst && kind?.toLowerCase().includes('field')) { + return vscode.CompletionItemKind.EnumMember + } + // Define a mapping from item types to completion item kinds const kinds: any = { + 'User Export Function': vscode.CompletionItemKind.Function, + 'User Method': vscode.CompletionItemKind.Method, + 'User Function': vscode.CompletionItemKind.Function, + 'User Export Type': vscode.CompletionItemKind.Class, + 'User Type': vscode.CompletionItemKind.Class, Function: vscode.CompletionItemKind.Function, Method: vscode.CompletionItemKind.Method, Local: vscode.CompletionItemKind.Module, @@ -183,17 +208,28 @@ export class PineCompletionProvider implements vscode.CompletionItemProvider { Control: vscode.CompletionItemKind.Keyword, Variable: vscode.CompletionItemKind.Variable, Boolean: vscode.CompletionItemKind.EnumMember, - Constant: vscode.CompletionItemKind.Enum, + Constant: vscode.CompletionItemKind.Constant, Type: vscode.CompletionItemKind.Class, Annotation: vscode.CompletionItemKind.Reference, Property: vscode.CompletionItemKind.Property, - Parameter: vscode.CompletionItemKind.TypeParameter, // Corrected to TypeParameter - Field: vscode.CompletionItemKind.Field, // Added Field kind + Parameter: vscode.CompletionItemKind.TypeParameter, + Field: vscode.CompletionItemKind.Field, + Enum: vscode.CompletionItemKind.Enum, + EnumMember: vscode.CompletionItemKind.EnumMember, + 'Literal String': vscode.CompletionItemKind.Text, // For "" string literals Other: vscode.CompletionItemKind.Value, } + // For each key in the mapping, if the kind includes the key, return the corresponding completion item kind + // Check for exact match first, then .includes() + const lowerKind = kind.toLowerCase() for (const key in kinds) { - if (kind.toLowerCase().includes(key.toLowerCase())) { + if (lowerKind === key.toLowerCase()) { + return kinds[key] + } + } + for (const key in kinds) { + if (lowerKind.includes(key.toLowerCase())) { return kinds[key] } } @@ -218,200 +254,6 @@ export class PineCompletionProvider implements vscode.CompletionItemProvider { } } - /** - * Provides completion items for method completions. - * @param document - The current document. - * @param position - The current position within the document. - * @param match - The text to match. - * @returns null - */ - async methodCompletions(document: vscode.TextDocument, position: vscode.Position, match: string) { - try { - const map = Class.PineDocsManager.getMap('methods', 'methods2') - - let namespace: string = '' - let funcName: string = '' - - if (match.includes('.')) { - const split = match.split('.') - if (split.length > 1) { - namespace = split.shift() ?? '' - funcName = split.join('.') ?? '' - } - } else { - return [] - } - - if (!namespace || Class.PineDocsManager.getAliases.includes(namespace)) { - return [] - } - - const lowerNamespace = namespace.toLowerCase() - const lowerFuncName = funcName.toLowerCase() - const fullName = `${lowerNamespace}.${lowerFuncName}` - - for (let [name, doc] of map.entries()) { - if (!doc.isMethod || name[0] === '*') { - continue - } - - let docNameSplitLast: string | null = null - if (name.includes('.')) { - const docNameSplit = name.split('.') - docNameSplitLast = docNameSplit.pop() ?? null - } else { - docNameSplitLast = name - } - - const namejoin = `${namespace}.${docNameSplitLast}` - const lowerNameJoin = namejoin.toLowerCase() - - if (lowerNamespace && docNameSplitLast) { - let typoTrack = 0 - let minorTypoCount = 0 - let matchIndex = 0 - - for (let i = 0; i < fullName.length; i++) { - const char = fullName[i] - const foundIndex = lowerNameJoin.indexOf(char, matchIndex) - - if (foundIndex === -1) { - typoTrack++ - if (typoTrack > 1) { - break - } - } else if (foundIndex !== matchIndex) { - minorTypoCount++ - if (minorTypoCount >= 3) { - break - } - matchIndex = foundIndex + 1 - } else { - matchIndex++ - } - } - - if (typoTrack > 1 || minorTypoCount >= 3) { - continue - } - - let nType = Helpers.identifyType(namespace) - let dType = Helpers.getThisTypes(doc) - - if (!nType || !dType) { - continue - } - - // Convert array types to a more consistent format - nType = nType.replace(/([\w.]+)\[\]/, 'array<$1>') - dType = dType.replace(/([\w.]+)\[\]/, 'array<$1>') - - // Normalize dType to one of the basic types if it includes any of 'array', 'matrix', 'map' - const basicTypes = ['array', 'matrix', 'map'] - const replacementTypes = ['any', 'type', 'array', 'matrix', 'map'] - - for (const t of basicTypes) { - if (dType.includes(t)) { - for (const r of replacementTypes) { - if (dType.includes(r) || dType === r) { - dType = t - break - } - } - break - } - } - - // Ensure types are strings and perform the final type check - if (typeof nType !== 'string' || typeof dType !== 'string') { - continue - } - - if (!nType.includes(dType)) { - continue - } - - const completionItem = await this.createCompletionItem(document, name, namespace, doc, position, false) - if (completionItem) { - this.completionItems.push(completionItem) - } - } - } - } catch (error) { - console.error('An error occurred:', error) - return [] - } - } - - /** - * Provides completion items for function completions. - * @param document - The current document. - * @param position - The current position within the document. - * @param match - The text to match. - * @returns null - */ - async functionCompletions(document: vscode.TextDocument, position: vscode.Position, match: string) { - try { - // Get the documentation map - const map = Class.PineDocsManager.getMap( - 'functions', - 'completionFunctions', - 'variables', - 'variables2', - 'constants', - 'UDT', - 'types', - 'imports', - 'controls', - 'annotations', - 'fields', - 'fields2', - ) - - const lowerMatch = match.toLowerCase() - const matchLength = match.length - - for (const [name, doc] of map.entries()) { - const lowerName = name.toLowerCase() - if (lowerName.startsWith(lowerMatch[0])) { - let minorTypoCount = 0 - let majorTypoCount = 0 - let matchIndex = 0 - - for (let i = 0; i < matchLength; i++) { - const char = lowerMatch[i] - const foundIndex = lowerName.indexOf(char, matchIndex) - - if (foundIndex === -1) { - majorTypoCount++ - if (majorTypoCount > 1) { - break - } - } else if (foundIndex !== matchIndex) { - minorTypoCount++ - if (minorTypoCount >= 3) { - break - } - matchIndex = foundIndex + 1 - } else { - matchIndex++ - } - } - - if (majorTypoCount <= 1 && minorTypoCount < 3) { - const completionItem = await this.createCompletionItem(document, name, null, doc, position, false) - if (completionItem) { - this.completionItems.push(completionItem) - } - } - } - } - } catch (error) { - console.error(error) - return [] - } - } - /** * Provides completion items for the main completions. * @param document - The current document. @@ -437,8 +279,25 @@ export class PineCompletionProvider implements vscode.CompletionItemProvider { return [] } - await this.functionCompletions(document, position, match) - await this.methodCompletions(document, position, match) + const allCompletions: CompletionDoc[] = [] + allCompletions.push(...Class.pineCompletionService.getGeneralCompletions(match)) + allCompletions.push(...Class.pineCompletionService.getMethodCompletions(match)) + allCompletions.push(...Class.pineCompletionService.getUdtConstructorCompletions(match)) + allCompletions.push(...Class.pineCompletionService.getInstanceFieldCompletions(match)) + + for (const completionDoc of allCompletions) { + const vscodeCompletionItem = await this.createCompletionItem( + document, + completionDoc.name, // This is correct, completionDoc.name is the specific name + completionDoc.namespace, // Correct + completionDoc, // Pass the whole CompletionDoc object as the 'doc' argument + position, + false, + ) + if (vscodeCompletionItem) { + this.completionItems.push(vscodeCompletionItem) + } + } if (this.completionItems.length > 0) { return new vscode.CompletionList(this.completionItems, true) @@ -495,37 +354,37 @@ export class PineCompletionProvider implements vscode.CompletionItemProvider { completionData.kind?.toLowerCase().includes('property') ) { itemKind = vscode.CompletionItemKind.Field - } else if (completionData.kind?.toLowerCase().includes('parameter')) { - itemKind = vscode.CompletionItemKind.Variable - } else { - itemKind = await this.determineCompletionItemKind(completionData.kind) - } + // Let determineCompletionItemKind handle all kind assignments consistently + // The specific if/else if here for field/property/parameter was overriding it. + itemKind = await this.determineCompletionItemKind(completionData.kind, completionData) + + const docData = completionData as CompletionDoc + const completionItem = await this.createCompletionItem( + document, + docData.name, + docData.namespace ?? null, + docData, + position, + true, + ) - const completionItem = await this.createCompletionItem( - document, - completionData.name, - null, - completionData, - position, - true, - ) + if (completionItem) { + completionItem.kind = itemKind + completionItem.sortText = `order${index.toString().padStart(4, '0')}` + this.completionItems.push(completionItem) + anyAdded = true + } + index++ + } - if (completionItem) { - completionItem.kind = itemKind - completionItem.sortText = `order${index.toString().padStart(4, '0')}` - this.completionItems.push(completionItem) - anyAdded = true + // Only clear completions if there are no more to suggest + if (!anyAdded) { + PineSharedCompletionState.clearCompletions() + return [] } - index++ - } - // Only clear completions if there are no more to suggest - if (!anyAdded) { - PineSharedCompletionState.clearCompletions() - return [] + return new vscode.CompletionList(this.completionItems) } - - return new vscode.CompletionList(this.completionItems) } catch (error) { console.error(error) PineSharedCompletionState.clearCompletions() @@ -926,48 +785,64 @@ export class PineInlineCompletionContext implements vscode.InlineCompletionItemP async argumentInlineCompletions( document: vscode.TextDocument, position: vscode.Position, - docs: Record[], + allSuggestionsForActiveArg: Record[], ) { try { - if (!docs || docs.length === 0) { - PineSharedCompletionState.clearCompletions() + this.completionItems = [] + if (!allSuggestionsForActiveArg || allSuggestionsForActiveArg.length === 0) { return [] } - const existingFields = new Set() - const linePrefix = document.lineAt(position).text.substring(0, position.character) - const existingPairsMatch = linePrefix.matchAll(/(\w+)=/g) // match fieldName= - for (const match of existingPairsMatch) { - if (match && match[1]) { - existingFields.add(match[1]) + const linePrefix = document.lineAt(position.line).text.substring(0, position.character) + const whatUserIsTypingMatch = linePrefix.match(/(\w*)$/) // What user is currently typing for the current argument + const whatUserIsTyping = whatUserIsTypingMatch ? whatUserIsTypingMatch[0].toLowerCase() : '' + + let bestSuggestionForInline: Record | null = null + + // Scenario 1: Cursor is immediately after '(' or ', ' (empty argument slot) + if (linePrefix.endsWith('(') || linePrefix.match(/,\s*$/)) { + // Suggest the first available field/param name (these names end with '=') + bestSuggestionForInline = allSuggestionsForActiveArg.find((s) => s.name.endsWith('=')) ?? null + } + // Scenario 2: User is typing something for the argument + else if (whatUserIsTyping) { + // Prioritize matching a field/param name that starts with what user is typing + bestSuggestionForInline = + allSuggestionsForActiveArg.find( + (s) => s.name.endsWith('=') && s.name.toLowerCase().startsWith(whatUserIsTyping), + ) ?? null + if (!bestSuggestionForInline) { + // If not matching a field/param name, try to match a value suggestion + bestSuggestionForInline = + allSuggestionsForActiveArg.find( + (s) => !s.name.endsWith('=') && s.name.toLowerCase().startsWith(whatUserIsTyping), + ) ?? null } } - let index = 0 - for (const completion of docs) { - if (existingFields.has(completion.name.replace('=', ''))) { - continue - } + // Scenario 3: Cursor is after "fieldname = " (i.e., linePrefix ends with "= " or just "=") + // Suggest a value for the current field. + else if (linePrefix.match(/=\s*$/)) { + // activeArg should be the LHS of '='. allSuggestionsForActiveArg are for this activeArg. + // We prefer a value suggestion (not ending with '=') + bestSuggestionForInline = allSuggestionsForActiveArg.find((s) => !s.name.endsWith('=')) ?? null + } - const completionItem = await this.createInlineCompletionItem( + if (bestSuggestionForInline) { + const inlineCompletion = await this.createInlineCompletionItem( document, - completion.name, + bestSuggestionForInline.name, null, - completion, + bestSuggestionForInline, position, true, ) - - if (completionItem) { - completionItem.insertText = `order${index.toString().padStart(4, '0')}` // Keep sortText if needed for ordering - this.completionItems.push(completionItem) + if (inlineCompletion) { + this.completionItems.push(inlineCompletion) } - index++ } - - PineSharedCompletionState.clearCompletions() return new vscode.InlineCompletionList(this.completionItems) } catch (error) { - console.error(error) + console.error('Error in argumentInlineCompletions (InlineContext):', error) return [] } } diff --git a/src/PineCompletionService.ts b/src/PineCompletionService.ts new file mode 100644 index 0000000..29cac73 --- /dev/null +++ b/src/PineCompletionService.ts @@ -0,0 +1,597 @@ +// src/PineCompletionService.ts +import * as vscode from 'vscode' // Keeping import in case CompletionDoc needs VS Code types later, or for kind mapping logic if moved back. +import { Class } from './PineClass' // Assuming PineDocsManager is accessed via Class +import { Helpers } from './index' // Assuming Helpers is needed for type checks or formatting within logic + +/** + * Define a shape for the documentation data returned by the service. + * This is a structured representation of a potential completion item's data. + */ +export interface CompletionDoc { + name: string // The name/text to be potentially suggested (e.g., "plot", "array.push", "color=", "color.red") + doc: any // The original documentation object from PineDocsManager + namespace: string | null // The namespace or instance name (e.g., "array", "myArray", "color") + isMethod?: boolean // True if it's a method + kind?: string // Original kind string from doc (e.g., "Function", "Method", "Variable", "Constant", "Field", "Parameter") + type?: string // Type string (e.g., "series float", "array", "color") + isConst?: boolean // Marker for constants/enum members + default?: any // Default value if applicable + description?: string // Direct description text + // Add any other properties needed by the providers to create VS Code items + + // Optional: Add sortText if needed for specific ordering (e.g., for arguments) + sortText?: string +} + +/** + * Provides core completion logic based on Pine Script documentation. + * Decouples documentation lookup and filtering from VS Code specific item creation. + */ +export class PineCompletionService { + private docsManager: typeof Class.PineDocsManager + + constructor(docsManager: typeof Class.PineDocsManager) { + this.docsManager = docsManager + } + + /** + * Helper to check for minor typos between a potential match and the target name. + * This implements the typo tolerance logic from the original code. + * @param potentialMatch - The text typed by the user. + * @param targetName - The full name from the documentation. + * @returns true if the targetName is a plausible match for the potentialMatch with minor typos. + */ + private checkTypoMatch(potentialMatch: string, targetName: string): boolean { + if (!potentialMatch) return true // Empty match matches everything (e.g., suggest all on empty line) + if (!targetName) return false + + const lowerPotential = potentialMatch.toLowerCase() + const lowerTarget = targetName.toLowerCase() + const potentialLength = lowerPotential.length + + let majorTypoCount = 0 // Number of characters in potentialMatch not found in targetName + let minorTypoCount = 0 // Number of characters found out of order + let targetIndex = 0 // Track position in targetName + + for (let i = 0; i < potentialLength; i++) { + const char = lowerPotential[i] + const foundIndex = lowerTarget.indexOf(char, targetIndex) + + if (foundIndex === -1) { + majorTypoCount++ + if (majorTypoCount > 1) { + return false // More than one character completely missing/wrong + } + } else { + if (foundIndex !== targetIndex) { + // Character found, but not at the expected next position. + // Count how many characters were skipped in the target. + minorTypoCount += foundIndex - targetIndex + if (minorTypoCount >= 3) { + return false // Too many out-of-order or skipped characters + } + } + targetIndex = foundIndex + 1 // Move target index forward past the found character + } + } + + // Additional checks could be added, e.g., if the targetName is excessively longer than potentialMatch + // if (lowerTarget.length > targetIndex + 3) return false; // If many chars left in target, might not be the intended item. + // Sticking to original logic's implied checks (major <= 1, minor < 3 based on how they were used). + return majorTypoCount <= 1 && minorTypoCount < 3 + } + + /** + * Retrieves and filters general completions (functions, variables, types, keywords, etc.) + * based on the input match (the word typed before the cursor). + */ + getGeneralCompletions(match: string): CompletionDoc[] { + const completions: CompletionDoc[] = [] + if (!match) { + // If match is empty, suggest all top-level items? Or return empty? + // Original code returned empty if match was null/empty *after* trim. Let's stick to that. + return completions // Or maybe suggest common keywords like `study`, `plot`, `if`? Needs a source for those. + // The original code includes 'controls', 'annotations', 'UDT', 'types' here. + // Let's fetch those maps and iterate them. + } + + // Get maps for general top-level suggestions + const mapsToSearch = this.docsManager.getMap( + 'functions', + 'completionFunctions', // Note: Original code included this, assuming it's a valid map key for functions + 'variables', + 'variables2', + 'constants', // Top-level constants like `na`, `barstate.islast` (these might also be under namespaces) + 'UDT', // User Defined Types/Enums + 'types', // Built-in types like 'integer', 'string' + 'imports', // The `import` keyword itself or imported modules? Unclear. Assuming symbols brought *in* by imports. + 'controls', // 'if', 'for', 'while', etc. + 'annotations', // '@version', '@study', etc. + // 'fields', // Global fields? Added to instanceFieldCompletions primarily. + // 'fields2', // Global fields? Added to instanceFieldCompletions primarily. + ) + + const lowerMatch = match.toLowerCase() + // const matchLength = match.length; // Not needed with checkTypoMatch + + for (const [name, doc] of mapsToSearch.entries()) { + if (!name || name[0] === '*') continue // Skip items with no name or starting with '*' (if any) + + const lowerName = name.toLowerCase() + + // Basic prefix check first for potential performance gain on large maps + // If the user typed "pl", only check items starting with "p" or "pl". + // Let's check if the name starts with the typed text OR if the typo match applies. + // This allows "plot" to match "plt" even if "plot" doesn't start with "plt". + if (lowerName.startsWith(lowerMatch) || this.checkTypoMatch(match, name)) { + completions.push({ + name: name, // The identifier name + doc: doc, // Original documentation object + namespace: null, // Top-level items have no namespace + isMethod: doc?.isMethod ?? false, + kind: doc?.kind, + type: doc?.type, + isConst: doc?.isConst, // For constants like `na` + default: doc?.default, // Default value for variables/constants + description: doc?.desc, // Description text + }) + } + } + return completions + } + + /** + * Retrieves and filters method completions based on the input match (e.g., "array.push"). + * Assumes match includes a dot. + */ + getMethodCompletions(match: string): CompletionDoc[] { + const completions: CompletionDoc[] = [] + // This method is called only if match includes a dot by the provider, but add check for safety. + if (!match || !match.includes('.')) { + return completions + } + + const parts = match.split('.') + // For `ns.part` match, `variableOrNamespace` is `ns`, `partialNameAfterDot` is `part`. + // For `ns.` match, `variableOrNamespace` is `ns`, `partialNameAfterDot` is ``. + const variableOrNamespace = parts[0] + const partialNameAfterDot = parts.length > 1 ? parts.slice(1).join('.') : '' // Allow dots in method names if needed, though unlikely + + if (!variableOrNamespace) { + // Needs at least `something.` + return completions + } + + const methodsMap = this.docsManager.getMap('methods', 'methods2') + // Original code had an alias check. Let's ignore it for now unless its purpose is clear. + // if (this.docsManager.getAliases().includes(variableOrNamespace)) return []; // This check seems counter-intuitive if aliases have methods. + + // Determine the type of the variable/namespace before the dot. + const potentialType = Helpers.identifyType(variableOrNamespace) + + for (const [name, doc] of methodsMap.entries()) { + if (!doc.isMethod || name[0] === '*') { + continue // Skip non-methods or internal items + } + + // Expected format in map is likely `namespace.methodName` + const nameParts = name.split('.') + if (nameParts.length < 2) continue // Skip items not in namespace.method format + const docNamespace = nameParts[0] + const docMethodName = nameParts.slice(1).join('.') // The actual method name part + + // --- Type Compatibility Check --- + // Check if the type of the object before the dot (`variableOrNamespace`'s type) + // is compatible with the method's expected `this` type (`Helpers.getThisTypes(doc)`). + const expectedThisTypes = Helpers.getThisTypes(doc) + + let typeMatch = false + if (expectedThisTypes) { + // Normalize types for comparison (e.g., `array` vs `array`) + const normalizedPotentialType = potentialType + ? potentialType.toLowerCase().replace(/([\w.]+)\[\]/g, 'array<$1>') + : null + const normalizedExpectedTypes = expectedThisTypes.toLowerCase().replace(/([\w.]+)\[\]/g, 'array<$1>') + + // Simple compatibility check: Does the potential type match or include the expected base type? + const expectedBaseType = normalizedExpectedTypes.split('<')[0] + const potentialBaseType = normalizedPotentialType ? normalizedPotentialType.split('<')[0] : null + + if (potentialBaseType === expectedBaseType) { + typeMatch = true // e.g. variable is `array`, method expects `array` -> base types match `array` + } else if (potentialType === null && variableOrNamespace === docNamespace) { + // If we couldn't identify a variable type (e.g. it's a namespace like `math`), + // check if the namespace itself matches the method's documented namespace. + typeMatch = true // e.g. user typed `math.`, method is `math.abs` -> namespace matches + } + // Add more sophisticated type compatibility checks if needed for Pine Script. + // For now, this basic check based on base type or namespace match replicates a plausible version of original intent. + } else { + // If the method has no documented `this` type, maybe it's a static method or global function namespaced? + // Assume it's a match if we can't check type compatibility, or if the namespace part matches explicitly. + // Let's require the documented namespace to match the typed namespace if type check fails. + if (variableOrNamespace === docNamespace) { + typeMatch = true + } else { + // If type check fails and namespace doesn't match, skip. + continue + } + } + + if (!typeMatch) { + continue // Skip methods that don't apply to this type/namespace context + } + // --- End Type Compatibility Check --- + + // Now check if the method name (`docMethodName`) matches the `partialNameAfterDot` (what user typed after dot) + // Use the typo check logic. + if (this.checkTypoMatch(partialNameAfterDot, docMethodName)) { + completions.push({ + name: `${variableOrNamespace}.${docMethodName}`, // Return the full name including namespace for display/insertion + doc: doc, // Original documentation object + namespace: variableOrNamespace, // The determined namespace/variable name + isMethod: true, // It's a method completion + kind: doc?.kind, // Original kind (should be Method) + type: doc?.type, // Return type of the method + description: doc?.desc, // Description text + }) + } + } + + return completions + } + + /** + * Retrieves and filters UDT constructor completions (e.g., MyType.new). + * Assumes match ends with a dot. + */ + getUdtConstructorCompletions(match: string): CompletionDoc[] { + const completions: CompletionDoc[] = [] + // This method is called only if match includes a dot by the provider, but add check for safety. + if (!match || !match.endsWith('.')) { + return completions + } + + const potentialUdtName = match.slice(0, -1) // Text before the dot + + if (!potentialUdtName) { + return completions + } + + const udtMap = this.docsManager.getMap('UDT', 'types') // UDT definitions are in these maps + + const udtDoc = udtMap.get(potentialUdtName) + + // Suggest 'new()' if the part before the dot is a known UDT name + // The user might type 'MyType.' and we suggest 'new()'. + // Or user might type 'MyType.ne' and we still suggest 'new()', filtered by the provider if needed, + // or perhaps this service should check if 'new' matches `partialNameAfterDot` if it existed? + // Original code only triggered if match ends with '.', suggesting 'new()' only when typing `MyType.`. + // Let's stick to suggesting 'new()' only when the match is `UdtName.`. + // The provider can filter this if the user types something like `MyType.ne`. + if (udtDoc) { + completions.push({ + name: 'new()', // The text to insert/suggest + doc: udtDoc, // Link to the UDT documentation + namespace: potentialUdtName, // The UDT name is the "namespace" for the constructor + kind: 'Constructor', // Define a kind for constructors + description: udtDoc?.desc || `Creates a new instance of \`${potentialUdtName}\`.`, // Use UDT desc or default + // If the doc structure includes constructor args, add them here. + // For now, assume basic 'new()' signature. + }) + } + + return completions + } + + /** + * Retrieves and filters instance field/property completions (e.g., myobj.fieldname). + * Also handles built-in namespace constants (e.g. color.red). + * Assumes match includes a dot. + */ + getInstanceFieldCompletions(match: string): CompletionDoc[] { + const completions: CompletionDoc[] = [] + // This method is called only if match includes a dot by the provider, but add check for safety. + if (!match || !match.includes('.')) { + return completions + } + + const parts = match.split('.') + // For `obj.part` match, `variableOrNamespace` is `obj`, `partialNameAfterDot` is `part`. + // For `obj.` match, `variableOrNamespace` is `obj`, `partialNameAfterDot` is ``. + const variableOrNamespace = parts[0] + const partialNameAfterDot = parts.length > 1 ? parts[1] : '' // Only the first part after the dot for now. Original code did this. + + if (!variableOrNamespace) { + // Needs at least `something.` + return completions + } + + // Limit depth for now to avoid `obj.field.another` + if (parts.length > 2 && !match.endsWith('.')) { + // If match is `obj.field.something`, we don't handle it. + // But if match is `obj.field.`, partialNameAfterDot would be '', parts.length is 3. + // The user might be typing `obj.field.` hoping to get members of `obj.field`'s type. + // The original code returned early if split('.') > 2 *unless* it ended with dot. + // Let's simplify: only process `var.part` format. + if (parts.length > 2) { + return completions + } + } + + const variablesMap = this.docsManager.getMap('variables', 'variables2') + const udtMap = this.docsManager.getMap('UDT', 'types') // For user-defined types/enums + const constantsMap = this.docsManager.getMap('constants') // For namespace constants like color.red + + let membersToIterate: any[] | null = null + let definitionDoc: any = null // The documentation for the type/namespace itself + let sourceKind: 'UDT' | 'VariableUDT' | 'BuiltInNamespace' | null = null + let resolvedTypeName: string | null = null // The type of the variable if found + + // 1. Check if variableOrNamespace is a known variable instance + const variableDoc = variablesMap.get(variableOrNamespace) + if (variableDoc && variableDoc.type) { + resolvedTypeName = variableDoc.type // Store the type name + // Use variableDoc.type directly, as it's narrowed to string here + definitionDoc = udtMap.get(variableDoc.type) // Look up the type definition (UDT/Enum) + if (definitionDoc && definitionDoc.fields) { + // Assuming UDT/Enum docs have a 'fields' array + membersToIterate = definitionDoc.fields // These are field docs {name, type, desc, etc} + sourceKind = 'VariableUDT' + } + } + + // 2. If not a variable, check if variableOrNamespace is a known UDT name (static fields? enum members?) + // This handles cases like `MyEnum.Value1`. + if (!definitionDoc) { + definitionDoc = udtMap.get(variableOrNamespace) // Check if it's a UDT/Enum type name directly + if (definitionDoc && definitionDoc.fields) { + // Assuming UDT/Enum docs have a 'fields' array + resolvedTypeName = variableOrNamespace // The type name itself + membersToIterate = definitionDoc.fields + sourceKind = 'UDT' + } + } + + // 3. If not a UDT name/instance, check for built-in namespaces with constants or methods + // This handles cases like `color.red`, `math.pi`. + if (!definitionDoc || !membersToIterate) { + // Collect constants that belong to this namespace prefix + const namespaceMembers: any[] = [] // Collects constants and potentially static methods here + const lowerVariableOrNamespace = variableOrNamespace.toLowerCase() + + // Check constants belonging to this namespace + for (const [constName, constDoc] of constantsMap.entries()) { + // Check if the constant's documented namespace matches OR if its full name starts with `namespace.` + // We need the *member name* relative to the namespace (e.g., 'red' for 'color.red'). + if ( + typeof constName === 'string' && + (constDoc.namespace === variableOrNamespace || + constName.toLowerCase().startsWith(lowerVariableOrNamespace + '.')) + ) { + const memberName = + constDoc.namespace === variableOrNamespace + ? constDoc.name // Use name directly if namespace matches exactly (might already be just the member name) + : constName.substring(variableOrNamespace.length + 1) // Extract part after `namespace.` + + // Filter now based on the partial name typed after the dot + if (memberName.toLowerCase().startsWith(partialNameAfterDot.toLowerCase())) { + namespaceMembers.push({ + name: memberName, // The member name (e.g., "red", "pi") + doc: constDoc, // Original constant doc + namespace: variableOrNamespace, // Keep track of the namespace + isConst: true, // Mark as constant + kind: constDoc.kind || 'Constant', // Use doc's kind or default + type: constDoc.type, + default: constDoc.syntax || constDoc.name, // Syntax might contain the value, or use name as value + description: constDoc.desc, + }) + } + } + } + + // Add static methods from this namespace if any? The original methodCompletions also handled namespaces. + // Maybe split method/field lookups entirely? Or combine them here if the source is a Namespace. + // Let's check methods map for methods starting with `namespace.` + const methodsMap = this.docsManager.getMap('methods', 'methods2') + for (const [methodFullName, methodDoc] of methodsMap.entries()) { + if ( + typeof methodFullName === 'string' && + methodDoc.isMethod && + methodFullName.toLowerCase().startsWith(lowerVariableOrNamespace + '.') + ) { + const methodRelativeName = methodFullName.substring(variableOrNamespace.length + 1) + // Filter based on partial name after dot + if (methodRelativeName.toLowerCase().startsWith(partialNameAfterDot.toLowerCase())) { + namespaceMembers.push({ + name: methodRelativeName + '()', // Suggest method name with parens + doc: methodDoc, + namespace: variableOrNamespace, + isMethod: true, + kind: methodDoc.kind, + type: methodDoc.type, // Return type + description: methodDoc.desc, + }) + } + } + } + + if (namespaceMembers.length > 0) { + membersToIterate = namespaceMembers // Treat namespace members (constants/static methods) as 'fields' in this context + sourceKind = 'BuiltInNamespace' + // We don't have a single 'definitionDoc' for the namespace itself from UDT map, but can use a placeholder. + // definitionDoc = { name: variableOrNamespace, doc: `Built-in namespace: ${variableOrNamespace}` }; + definitionDoc = null // No single doc for the namespace's definition + } + } + + if (!membersToIterate) { + // No matching variable type, UDT, or namespace found with members + return completions + } + + // Iterate over the collected members and create CompletionDoc objects + for (const member of membersToIterate) { + // For BuiltInNamespace members, filtering by partialNameAfterDot was already done above. + // For UDT fields, filter here if partialNameAfterDot exists. + if ( + sourceKind !== 'BuiltInNamespace' && + partialNameAfterDot && + !member.name.toLowerCase().startsWith(partialNameAfterDot.toLowerCase()) + ) { + continue + } + + // For UDT fields, the member object might be just { name, type }. + // For constants/methods added from namespace check, the member object is the full doc. + // Need to structure the CompletionDoc consistently. + const compDoc: CompletionDoc = { + name: member.name, // The field/member name or method name + () + doc: member.doc || member, // Use member.doc if available (from namespace search), otherwise the member object itself (for UDT fields) + namespace: variableOrNamespace, // The object/namespace name before the dot + isMethod: member.isMethod ?? false, + kind: + member.kind || + (sourceKind === 'UDT' || sourceKind === 'VariableUDT' ? 'Field' : member.isConst ? 'Constant' : 'Value'), // Infer kind if missing + type: member.type, // Type of the field/member/method return + isConst: member.isConst ?? (sourceKind === 'BuiltInNamespace' && !member.isMethod), // Mark as const if doc says so, or if it's a non-method from BuiltInNamespace + default: member.default, // Default value if available + description: member.description || member.desc, // Use explicit description if available + // For UDT fields, description needs extraction from parent UDT doc. + } + + completions.push(compDoc) + } + + return completions + } + + /** + * Retrieves argument suggestions for a specific function/method based on the provided docs. + * Assumes the input `argDocs` is the list of argument documentation objects + * for the function currently being called. + * Filters based on existing arguments already typed on the line prefix. + */ + getArgumentCompletions(argDocs: any[], linePrefix: string): CompletionDoc[] { + const completions: CompletionDoc[] = [] + if (!argDocs || argDocs.length === 0) { + return [] + } + + const existingFields = new Set() + // Find already typed named arguments (e.g., `color=`, `style=`) + // Use a more robust regex to find named args within the current function call scope. + // This requires sophisticated parsing to know the current call scope, which is hard with just `linePrefix`. + // Assuming `linePrefix` is text up to cursor within the argument list. + // Regex finds `word = ` before the cursor. + const namedArgMatch = linePrefix.match(/(\w+)\s*=\s*[^,\)]*$/) // Matches `argName = value_part_being_typed` + const lastCommaMatch = linePrefix.lastIndexOf(',') + const openParenMatch = linePrefix.lastIndexOf('(') + const lastDelimiterPos = Math.max(lastCommaMatch, openParenMatch) + const argsText = lastDelimiterPos >= 0 ? linePrefix.substring(lastDelimiterPos + 1) : linePrefix // Text after last ( or , + + // Find all already-typed named arguments in the current arg list context + const existingNamedArgsMatch = argsText.matchAll(/(\w+)\s*=/g) + for (const match of existingNamedArgsMatch) { + if (match && match[1]) { + existingFields.add(match[1]) // Add the name (LHS of =) + } + } + + // Find the partial text being typed for the current argument + const partialMatchInCurrentArg = argsText.match(/(\w*)$/) + const lowerPartialText = partialMatchInCurrentArg ? partialMatchInCurrentArg[1].toLowerCase() : '' + + let index = 0 // For sorting suggestions in order of definition + for (const argDoc of argDocs) { + // argDoc format is expected to be like {name: 'series', type: 'series float'}, or {name: 'title=', type: 'string', default: "Plot", kind: 'Parameter'} + const argName = argDoc.name?.replace('=', '')?.trim() // The base name (e.g., 'title' from 'title=') + + // Skip if this named argument already exists + if (argName && existingFields.has(argName)) { + continue + } + + // If the user is typing something, filter suggestions by the typed text. + // This applies to both named argument suggestions ('title=') and value suggestions ('color.red'). + const suggestionText = argDoc.name || '' // Text to suggest ('title=' or 'color.red') + if (lowerPartialText && !suggestionText.toLowerCase().startsWith(lowerPartialText)) { + continue // Skip if suggestion doesn't start with typed text + } + + // Decide the display name and insert text. + // For named args like {name: 'title='}, suggestionText is 'title='. This is the insert text. + // For positional args or value suggestions like {name: 'color.red'}, suggestionText is 'color.red'. This is the insert text. + // For positional args like {name: 'series', type: 'series float'}, the user needs to type a *value*. + // We might need to suggest common values or variables of the correct type. + // The current argDocs format seems to mix positional argument names and named argument suggestions/values. + // Assuming `argDoc.name` is the literal text to suggest/insert ('series', 'title=', 'color.red'). + + completions.push({ + name: suggestionText, // The text to insert/suggest (e.g., "series", "title=", "color.red") + doc: argDoc, // Original argument doc + namespace: null, // Arguments don't typically have a namespace here + kind: argDoc.kind || (argDoc.name?.endsWith('=') ? 'Field' : 'Parameter'), // Infer kind: 'Field' for named arg ('name='), 'Parameter' for positional? Or rely on original kind. Use Parameter if missing. + type: argDoc.type, // Type of the argument + isConst: argDoc.isConst, + default: argDoc.default, + description: argDoc.desc, // Description of the argument + // Add sortText for ordering + sortText: `order${index.toString().padStart(4, '0')}`, // Ensure correct order + }) + index++ + } + + return completions + } + + /** + * Attempts to find the documentation for a function, method, or UDT constructor + * by its name (which could be simple like "plot" or namespaced like "array.push" or "MyType.new"). + * This is useful for providers (like Signature Help or Inline Completion context detection) + * that need the full documentation for a known callable. + * @param name The name of the function, method (namespace.name), or UDT constructor (UDT.new). + * @returns The documentation object if found, otherwise undefined. + */ + getFunctionDocs(name: string): any | undefined { + if (!name) return undefined + + // Normalize name for lookup (e.g., remove trailing ()) + const searchName = name.replace(/\(\)$/, '') // Remove () if present + + // Check common sources for callable documentation + const functionMap = this.docsManager.getMap('functions', 'completionFunctions') + if (functionMap.has(searchName)) { + return functionMap.get(searchName) + } + + // Check methods map (methods are typically namespaced) + const methodMap = this.docsManager.getMap('methods', 'methods2') + if (methodMap.has(searchName)) { + return methodMap.get(searchName) + } + + // Check UDTs for constructor (.new) + if (searchName.endsWith('.new')) { + const udtName = searchName.slice(0, -'.new'.length) + const udtMap = this.docsManager.getMap('UDT', 'types') + const udtDoc = udtMap.get(udtName) + if (udtDoc) { + // Return the UDT doc, potentially augmenting it to signify constructor + // The structure needed by Signature Help might be specific. + // For now, just return the UDT doc. Signature Help needs args list. + // Assuming UDT doc has an `args` property for the constructor, or it's derived from fields/syntax. + return { ...udtDoc, name: `${udtName}.new`, kind: 'Constructor', args: udtDoc.args || [] } // Ensure args is an array + } + } + + // Add checks for namespaced functions in 'functions' map if they exist there (unlikely, but possible) + // Example: math.abs might be in functions map as 'math.abs' + if (searchName.includes('.')) { + const funcDoc = functionMap.get(searchName) + if (funcDoc) return funcDoc + } + + return undefined // Not found + } +} diff --git a/src/PineDocString.ts b/src/PineDocString.ts index 96c5ba4..48030fa 100644 --- a/src/PineDocString.ts +++ b/src/PineDocString.ts @@ -7,6 +7,8 @@ export class PineDocString { functionPattern = /(?:export\s+)?(?:method\s+)?([\w.]+)\s*\(.*\)\s*=>/g // Regex pattern to match type declarations typePattern = /(?:export\s+)?(?:type)\s+([\w.]+)/g + // Regex pattern to match enum declarations + enumPattern = /(?:export\s+)?(?:enum)\s+([\w.]+)/g; Editor: vscode.TextEditor | undefined /** @@ -59,14 +61,49 @@ export class PineDocString { } const desc = docsMatch.desc || docsMatch.info || '...' - const docStringBuild = [`// @type ${match} - ${desc}`] + const docStringBuild = [`// @type \`${match}\` - ${desc}`] docsMatch.fields.forEach((field: any) => { - docStringBuild.push( - `// @field ${field.name} *${Helpers.replaceType(field?.type || '')}* - ${field?.desc || field?.info || '...'} ${ - field.default ? ' (' + field.default + ')' : '' - }`, - ) + const fieldType = Helpers.replaceType(field?.type || '') + const fieldDescription = field?.desc || field?.info || '...' + let fieldDoc = `// @field ${field.name} (${fieldType}) ${fieldDescription}.` + // Handle cases where UDT fields might not have explicit default values + if (field.default && !['array', 'matrix', 'map'].some(t => fieldType.startsWith(t))) { + fieldDoc += ` defval = ${field.default}` + } + docStringBuild.push(fieldDoc) + }) + + return docStringBuild.join('\n') + } + + /** + * Generates a docstring for a given enum declaration code. + * @param match The string of the enum's code. + * @returns The generated docstring. + */ + async generateEnumDocstring(match: string): Promise { + // Assuming enums might be stored similarly to UDTs or a new map like Class.PineDocsManager.getMap('enums') + // For now, let's try 'UDT' first. + const enumData: Map> = Class.PineDocsManager.getMap('UDT') + const docsMatch = enumData.get(match) + + if (!docsMatch) { + // If not found in 'UDT', one might check 'enums' or other specific maps if available. + // For this example, we'll indicate if it's not found. + return `// Enum details for ${match} not found.` + } + + const desc = docsMatch.desc || docsMatch.info || '...' + const docStringBuild = [`// @enum \`${match}\` - ${desc}`] + + // Assuming enum fields (values) are in a property like 'fields' or 'values' + // This might need adjustment based on actual data structure for enums + const enumFields = docsMatch.fields || docsMatch.values || [] + + enumFields.forEach((field: any) => { + const fieldDescription = field?.desc || field?.info || '...' + docStringBuild.push(`// @field ${field.name} Represents ${fieldDescription}.`) }) return docStringBuild.join('\n') @@ -90,16 +127,20 @@ export class PineDocString { // Define patterns and their corresponding methods in an array const patterns = [ - { pattern: this.functionPattern, method: this.generateDocstring }, - { pattern: this.typePattern, method: this.generateTypeDocstring }, + { pattern: this.functionPattern, method: this.generateDocstring.bind(this) }, + { pattern: this.typePattern, method: this.generateTypeDocstring.bind(this) }, + { pattern: this.enumPattern, method: this.generateEnumDocstring.bind(this) }, // Added enum pattern ] for (let i = 0; i < patterns.length; i++) { + // Reset lastIndex for global regex patterns to avoid issues with subsequent exec calls + patterns[i].pattern.lastIndex = 0 let match = patterns[i].pattern.exec(code) if (match?.[1]) { finishedDocstring = await patterns[i].method(match[1].trim()) if (!finishedDocstring) { - finishedDocstring = '// Invalid function match' + // It's better to have a more specific message or rely on the individual generators to return appropriate messages + finishedDocstring = `// Could not generate docstring for ${match[1].trim()}` } break // Exit the loop once a match is found } diff --git a/src/PineDocsManager.ts b/src/PineDocsManager.ts index a5ba766..9b57c53 100644 --- a/src/PineDocsManager.ts +++ b/src/PineDocsManager.ts @@ -382,34 +382,61 @@ export class PineDocsManager { for (const doc of docs) { const { name } = doc - let currentDocs = currentMap.get(name) + let currentDocEntry = currentMap.get(name) - if (currentDocs && doc[keyType] && doc[keyType].length > 0) { - // Ensure the currentDocs[keyType] exists and is an array. - if (!Array.isArray(currentDocs[keyType])) { - currentDocs[keyType] = [] - } + if (currentDocEntry) { + // Update existing entry + // Prioritize details from PineParser (doc) for user-defined functions + currentDocEntry.kind = doc.kind || currentDocEntry.kind; + currentDocEntry.body = doc.body || currentDocEntry.body; + currentDocEntry.doc = doc.doc || currentDocEntry.doc; // Parsed docstring - for (let arg of doc[keyType]) { - const argName = arg.name - let currentArg = currentDocs[keyType].find((a: any) => a.name === argName) + if (doc.export !== undefined) { + currentDocEntry.export = doc.export; + } + if (doc.method !== undefined) { + currentDocEntry.method = doc.method; + } - if (currentArg) { - // Update properties of the existing argument. - currentArg.required = arg.required - if (arg.default) { - currentArg.default = arg.default - } - if (currentArg.type === 'undefined type') { - currentArg.type = arg.type + // Merge arguments (keyType is 'args' for functions) or fields (keyType is 'fields' for UDTs) + if (doc[keyType] && doc[keyType].length > 0) { + if (!Array.isArray(currentDocEntry[keyType])) { + currentDocEntry[keyType] = [] + } + for (let parsedMember of doc[keyType]) { // Can be an argument or a field + const memberName = parsedMember.name + let currentMember = currentDocEntry[keyType].find((m: any) => m.name === memberName) + + if (currentMember) { + // Update properties of the existing member (arg or field) + currentMember.type = parsedMember.type || currentMember.type; + currentMember.kind = parsedMember.kind || currentMember.kind; // Ensure kind is updated + if (parsedMember.default !== undefined) { // Check for undefined to allow setting null or empty string defaults + currentMember.default = parsedMember.default + } + if (parsedMember.isConst !== undefined) { // Ensure isConst is updated + currentMember.isConst = parsedMember.isConst; + } + if (keyType === 'args') { // Argument-specific properties + currentMember.required = parsedMember.required; // Note: UDT fields don't typically have 'required' in the same way as func args + if (parsedMember.modifier) { + currentMember.modifier = parsedMember.modifier; + } + } + } else { + // Add new member if not found + currentDocEntry[keyType].push(parsedMember); } } } - // Update the map with the modified document. - currentMap.set(name, currentDocs) + currentMap.set(name, currentDocEntry) + } else if (keyType === 'args' || keyType === 'fields') { + // Add new entry if it doesn't exist in the map (for functions/args or UDTs/fields) + currentMap.set(name, doc); } } // Save the updated map. + // The structure expected by setDocs is [{ docs: [...] }] this.setDocs([{ docs: Array.from(currentMap.values()) }], k) } } catch (error) { diff --git a/src/PineInlineCompletionContext.ts b/src/PineInlineCompletionContext.ts new file mode 100644 index 0000000..ead3237 --- /dev/null +++ b/src/PineInlineCompletionContext.ts @@ -0,0 +1,459 @@ +// src/PineInlineCompletionContext.ts +import { Helpers, PineSharedCompletionState } from './index' // Assuming these are correctly defined elsewhere +import { Class } from './PineClass' // Assuming PineDocsManager is accessed via Class +import * as vscode from 'vscode' +// PineCompletionService and CompletionDoc are not used in this file. + +function buildLabel( + name: string, + doc: any, + namespace: string | null, +): { label: string; openParen: string; closeParen: string } { + let label = name + let openParen = '' + let closeParen = '' + const kind = doc?.kind + if (kind && (kind.includes('Function') || kind.includes('Method'))) { + label = name.replace(/\(\)/g, '') + openParen = '(' + closeParen = ')' + } + if (doc?.isMethod && namespace) { + label = `${namespace}.${label.split('.').pop()}` + } + return { label: label + openParen + closeParen, openParen, closeParen } +} + +async function safeExecute(action: () => Promise, fallback: T): Promise { + try { + return await action() + } catch (error) { + console.error(error) + return fallback + } +} +export class PineInlineCompletionContext implements vscode.InlineCompletionItemProvider { + completionItems: vscode.InlineCompletionItem[] = [] + docType: any + userDocs: any + map: any + isMapNew: any + namespaces: any + match: string | undefined = undefined + activeArg: string | null = null + argumentCompletionsFlag: boolean = false + sigCompletions: Record = {} + + /** + * Checks if completions are available for the current context. + * @returns An array of completions. + */ + checkCompletions(): Record[] { + try { + const activeArg = PineSharedCompletionState.getActiveArg + if (PineSharedCompletionState.getArgumentCompletionsFlag && activeArg) { + PineSharedCompletionState.setArgumentCompletionsFlag(false) + return PineSharedCompletionState.getCompletions[activeArg] ?? [] + } + return [] + } catch (error) { + console.error(error) + return [] + } + } + + /** + * Provides inline completion items for the current position in the document. + * @param document - The current document. + * @param position - The current position within the document. + * @param context - The inline completion context. + * @param token - A cancellation token. + * @returns An array of inline completion items. + */ + async provideInlineCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + context: vscode.InlineCompletionContext, + token: vscode.CancellationToken, + ): Promise { + try { + // Initialize the completion items array + this.completionItems = [] + + const completionsFromState: Record[] = this.checkCompletions() + + if (token.isCancellationRequested) { + } + + if (completionsFromState.length > 0) { + return await this.argumentInlineCompletions(document, position, completionsFromState) + } else { + return await this.mainInlineCompletions(document, position) + } + } catch (error) { + console.error(error) + return [] + } + } + + /** + * Creates an inline completion item for the given name and documentation. + * @param document - The current document. + * @param name - The name of the item. + * @param namespace - The namespace of the item, if it's a method. + * @param doc - The documentation for the item. + * @param position - The current position within the document. + * @param argCompletion - A flag indicating whether this is an argument completion. + * @returns A InlineCompletionItem object. + */ + async createInlineCompletionItem( + document: vscode.TextDocument, + name: string, + namespace: string | null, + doc: any, + position: vscode.Position, + argCompletion: boolean = false, + ): Promise { + return safeExecute(async () => { + const { label } = buildLabel(name, doc, namespace) + let insertText = label + const textBeforeCursor = document.lineAt(position.line).text.substring(0, position.character) + let startPosition: number + if (argCompletion) { + const argStartMatch = /(?:\(|,)?\s*\b[\w.]+$/.exec(textBeforeCursor) + startPosition = Math.max(position.character - (argStartMatch ? argStartMatch[0].length : 0), 0) + } else { + const wordStartMatch = /\b[\w.]+$/.exec(textBeforeCursor) + startPosition = Math.max(position.character - (wordStartMatch ? wordStartMatch[0].length : 0), 0) + } + return new vscode.InlineCompletionItem( + insertText, + new vscode.Range(new vscode.Position(position.line, startPosition), position), + ) + }, null) + } + + /** + * Provides inline completion items for method completions. + * @param document - The current document. + * @param position - The current position within the document. + * @param match - The text to match. + * @returns null + */ + async methodInlineCompletions(document: vscode.TextDocument, position: vscode.Position, match: string) { + try { + const map = Class.PineDocsManager.getMap('methods', 'methods2') + + let namespace: string = '' + let funcName: string = '' + + if (match.includes('.')) { + const split = match.split('.') + if (split.length > 1) { + namespace = split.shift() ?? '' + funcName = split.join('.') ?? '' + } + } else { + return [] + } + + if (!namespace || Class.PineDocsManager.getAliases.includes(namespace)) { + return [] + } + + const lowerNamespace = namespace.toLowerCase() + const lowerFuncName = funcName.toLowerCase() + const fullName = `${lowerNamespace}.${lowerFuncName}` + + for (let [name, doc] of map.entries()) { + if (!doc.isMethod || name[0] === '*') { + continue + } + + let docNameSplitLast: string | null = null + if (name.includes('.')) { + const docNameSplit = name.split('.') + docNameSplitLast = docNameSplit.pop() ?? null + } else { + docNameSplitLast = name + } + + const namejoin = `${namespace}.${docNameSplitLast}` + const lowerNameJoin = namejoin.toLowerCase() + + if (lowerNamespace && docNameSplitLast) { + let typoTrack = 0 + let minorTypoCount = 0 + let matchIndex = 0 + + for (let i = 0; i < fullName.length; i++) { + const char = fullName[i] + const foundIndex = lowerNameJoin.indexOf(char, matchIndex) + + if (foundIndex === -1) { + typoTrack++ + if (typoTrack > 1) { + break + } + } else if (foundIndex !== matchIndex) { + minorTypoCount++ + if (minorTypoCount >= 3) { + break + } + matchIndex = foundIndex + 1 + } else { + matchIndex++ + } + } + + if (typoTrack > 1 || minorTypoCount >= 3) { + continue + } + + let nType = Helpers.identifyType(namespace) + let dType = Helpers.getThisTypes(doc) + + if (!nType || !dType) { + continue + } + + // Convert array types to a more consistent format + nType = nType.replace(/([\w.]+)\[\]/, 'array<$1>') + dType = dType.replace(/([\w.]+)\[\]/, 'array<$1>') + + // Normalize dType to one of the basic types if it includes any of 'array', 'matrix', 'map' + const basicTypes = ['array', 'matrix', 'map'] + const replacementTypes = ['any', 'type', 'array', 'matrix', 'map'] + + for (const t of basicTypes) { + if (dType.includes(t)) { + for (const r of replacementTypes) { + if (dType.includes(r) || dType === r) { + dType = t + break + } + } + break + } + } + + // Ensure types are strings and perform the final type check + if (typeof nType !== 'string' || typeof dType !== 'string') { + continue + } + + if (!nType.includes(dType)) { + continue + } + + const completionItem = await this.createInlineCompletionItem(document, name, namespace, doc, position, false) + if (completionItem) { + this.completionItems.push(completionItem) + } + } + } + } catch (error) { + console.error('An error occurred:', error) + return [] + } + } + + /** + * Provides inline completion items for function completions. + * @param document - The current document. + * @param position - The current position within the document. + * @param match - The text to match. + * @returns null + */ + async functionInlineCompletions(document: vscode.TextDocument, position: vscode.Position, match: string) { + try { + // Get the documentation map + const map = Class.PineDocsManager.getMap( + 'functions', + 'completionFunctions', + 'variables', + 'variables2', + 'constants', + 'UDT', + 'types', + 'imports', + 'controls', + 'annotations', + 'fields', + 'fields2', + ) + + const lowerMatch = match.toLowerCase() + const matchLength = match.length + + for (const [name, doc] of map.entries()) { + const lowerName = name.toLowerCase() + if (lowerName.startsWith(lowerMatch[0])) { + let minorTypoCount = 0 + let majorTypoCount = 0 + let matchIndex = 0 + + for (let i = 0; i < matchLength; i++) { + const char = lowerMatch[i] + const foundIndex = lowerName.indexOf(char, matchIndex) + + if (foundIndex === -1) { + majorTypoCount++ + if (majorTypoCount > 1) { + break + } + } else if (foundIndex !== matchIndex) { + minorTypoCount++ + if (minorTypoCount >= 3) { + break + } + matchIndex = foundIndex + 1 + } else { + matchIndex++ + } + } + + if (majorTypoCount <= 1 && minorTypoCount < 3) { + const completionItem = await this.createInlineCompletionItem(document, name, null, doc, position, false) + if (completionItem) { + this.completionItems.push(completionItem) + } + } + } + } + } catch (error) { + console.error(error) + return [] + } + } + + /** + * Provides inline completion items for the main completions. + * @param document - The current document. + * @param position - The current position within the document. + * @returns An array of inline completion items + */ + async mainInlineCompletions(document: vscode.TextDocument, position: vscode.Position) { + try { + // Get the text on the current line up to the cursor position + const line = document.lineAt(position) + if (line.text.trim().startsWith('//') || line.text.trim().startsWith('import')) { + return [] + } + + const linePrefix = line.text.substring(0, position.character) + // If there's no text before the cursor, return an empty array + if (!linePrefix) { + return [] + } + + // Check if we are right after an opening parenthesis (possibly with whitespace) + const argumentContextRegex = /\([\s]*$/ + if (argumentContextRegex.test(linePrefix.trim())) { + // Trigger argument completions directly + const functionCallMatch = linePrefix.trim().match(/(\w+)\([\s]*$/) // Capture function name if needed for context + if (functionCallMatch && functionCallMatch[1]) { + const functionName = functionCallMatch[1] + const functionDoc = Class.PineDocsManager.getFunctionDocs(functionName) // Use the new getFunctionDocs + + if (functionDoc && functionDoc.args) { + // Check if functionDoc and args exist + PineSharedCompletionState.setCompletions(functionDoc.args) // Set completions from functionDoc.args + PineSharedCompletionState.setArgumentCompletionsFlag(true) // Ensure flag is set + return await this.argumentInlineCompletions(document, position, functionDoc.args) // Pass functionDoc.args to argumentInlineCompletions + } + } + return [] // If no function name or args found in this context, return empty + } + + // If there are no completions in the shared state, match the text before the cursor + const match = linePrefix.match(/[\w.]+$/)?.[0].trim() + if (!match) { + return [] + } + + await this.functionInlineCompletions(document, position, match) + await this.methodInlineCompletions(document, position, match) + + if (this.completionItems.length > 0) { + return new vscode.InlineCompletionList(this.completionItems) + } + } catch (error) { + console.error(error) + return [] + } + } + + /** + * Provides inline completion items for argument completions. + * @param document - The current document. + * @param position - The current position within the document. + * @param docs - The documentation for the arguments. + * @returns An array of inline completion items. + */ + async argumentInlineCompletions( + document: vscode.TextDocument, + position: vscode.Position, + allSuggestionsForActiveArg: Record[], + ) { + try { + this.completionItems = [] + if (!allSuggestionsForActiveArg || allSuggestionsForActiveArg.length === 0) { + return [] + } + + const linePrefix = document.lineAt(position.line).text.substring(0, position.character) + const whatUserIsTypingMatch = linePrefix.match(/(\w*)$/) // What user is currently typing for the current argument + const whatUserIsTyping = whatUserIsTypingMatch ? whatUserIsTypingMatch[0].toLowerCase() : '' + + let bestSuggestionForInline: Record | null = null + + // Scenario 1: Cursor is immediately after '(' or ', ' (empty argument slot) + if (linePrefix.endsWith('(') || linePrefix.match(/,\s*$/)) { + // Suggest the first available field/param name (these names end with '=') + bestSuggestionForInline = allSuggestionsForActiveArg.find((s) => s.name.endsWith('=')) ?? null + } + // Scenario 2: User is typing something for the argument + else if (whatUserIsTyping) { + // Prioritize matching a field/param name that starts with what user is typing + bestSuggestionForInline = + allSuggestionsForActiveArg.find( + (s) => s.name.endsWith('=') && s.name.toLowerCase().startsWith(whatUserIsTyping), + ) ?? null + if (!bestSuggestionForInline) { + // If not matching a field/param name, try to match a value suggestion + bestSuggestionForInline = + allSuggestionsForActiveArg.find( + (s) => !s.name.endsWith('=') && s.name.toLowerCase().startsWith(whatUserIsTyping), + ) ?? null + } + } + // Scenario 3: Cursor is after "fieldname = " (i.e., linePrefix ends with "= " or just "=") + // Suggest a value for the current field. + else if (linePrefix.match(/=\s*$/)) { + // activeArg should be the LHS of '='. allSuggestionsForActiveArg are for this activeArg. + // We prefer a value suggestion (not ending with '=') + bestSuggestionForInline = allSuggestionsForActiveArg.find((s) => !s.name.endsWith('=')) ?? null + } + + if (bestSuggestionForInline) { + const inlineCompletion = await this.createInlineCompletionItem( + document, + bestSuggestionForInline.name, + null, + bestSuggestionForInline, + position, + true, + // Removed extra argument to match the expected function signature + ) + if (inlineCompletion) { + this.completionItems.push(inlineCompletion) + } + } + return new vscode.InlineCompletionList(this.completionItems) + } catch (error) { + console.error('Error in argumentInlineCompletions (InlineContext):', error) + return [] + } + } +} diff --git a/src/PineLint.ts b/src/PineLint.ts index ba0b43c..5f1423b 100644 --- a/src/PineLint.ts +++ b/src/PineLint.ts @@ -2,7 +2,6 @@ import { debounce } from 'lodash' import * as vscode from 'vscode' import { VSCode } from './VSCode' import { Class } from './PineClass' - /** * PineLint class is responsible for linting Pine Script code. */ @@ -106,7 +105,21 @@ export class PineLint { * @param dataGroups - The groups of data to update the diagnostics with. */ static async updateDiagnostics(...dataGroups: any[][]): Promise { + const activeEditor = vscode.window.activeTextEditor + if (!activeEditor) { + return + } + const documentUri = activeEditor.document.uri + const targetEditor = vscode.window.visibleTextEditors.find( + (editor) => editor.document.uri.toString() === documentUri.toString(), + ) + if (!targetEditor) { + return + } + const diagnostics: vscode.Diagnostic[] = [] + const errorDecorationRanges: vscode.Range[] = [] + const warningDecorationRanges: vscode.Range[] = [] let i = 0 for (const group of dataGroups) { i += 1 diff --git a/src/PineParser.ts b/src/PineParser.ts index 08ea860..6577ba6 100644 --- a/src/PineParser.ts +++ b/src/PineParser.ts @@ -12,15 +12,15 @@ export class PineParser { // Refactored regular expressions with named capture groups for better readability and maintainability // Type Definition Pattern typePattern: RegExp = - /(?(?(^\/\/\s*(?:@(?:type|field)[^\n]*))+(?=^((?:method\s+)?(export\s+)?)?\w+))?((export)?\s*(type)\s*(?\w+)\n(?(?:(?:\s+[^\n]+)\n+|\s*\n)+)))(?=(?:\b|^\/\/\s*@|(?:^\/\/[^@\n]*?$)+|$))/gm + /(?(?(?:^\/\/\s*(?:@(?:type|field)[^\n]*\n))+)?(?export)?\s*(type)\s*(?\w+)\n(?(?:(?:\s+[^\n]+)\n+|\s*\n)+))(?=(?:\b|^\/\/\s*@|(?:^\/\/[^@\n]*?$)+|$))/gm // Fields Pattern within Type Definition fieldsPattern: RegExp = - /^\s+(?:(?:(?:(array|matrix|map)<(?(?([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*)),)?(?([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*)))>)|(?([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*))((?\[\])?)\s+)?(?[a-zA-Z_][a-zA-Z0-9_]*)(?:(?=\s*=\s*)(?:(?'.*')|(?".*")|(?\d*(\.(\d+[eE]?\d+)?\d*|\d+))|(?#[a-fA-F0-9]{6,8})|(?([a-zA-Z_][a-zA-Z0-9_]*\.)*[a-zA-Z_][a-zA-Z0-9_]*)))?$/gm + /^\s+(?const\s+)?(?:(?:(?:(array|matrix|map)<(?(?([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*)),)?(?([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*)))>)|(?([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*))((?\[\])?)\s+)?(?[a-zA-Z_][a-zA-Z0-9_]*)(?:(?=\s*=\s*)(?:(?'.*')|(?".*")|(?\d*(\.(\d+[eE]?\d+)?\d*|\d+))|(?#[a-fA-F0-9]{6,8})|(?([a-zA-Z_][a-zA-Z_0-9]*\.)*[a-zA-Z_][a-zA-Z0-9_]*)))?$/gm // Function Definition Pattern funcPattern: RegExp = - /(\/\/\s*@f(?:@?.*\n)+?)?(?export)?\s*(?method)?\s*(?\w+)\s*\(\s*(?[^\)]+?)\s*\)\s*?=>\s*?(?(?:.*\n+)+?)(?=^\b|^\/\/\s*\@|$)/gm + /(?(?:\/\/\s*@f(?:@?.*\n)+?)?)?(?export)?\s*(?method)?\s*(?\w+)\s*\(\s*(?[^\)]+?)\s*\)\s*?=>\s*?(?(?:.*\n+)+?)(?=^\b|^\/\/\s*\@|$)/gm // Function Argument Pattern funcArgPattern: RegExp = @@ -181,7 +181,7 @@ export class PineParser { const functionMatches = script.matchAll(this.funcPattern) for (const funcMatch of functionMatches) { - const { functionName, parameters, body } = funcMatch.groups! // Non-null assertion is safe due to regex match + const { docstring, exportKeyword, methodKeyword, functionName, parameters, body } = funcMatch.groups! // Non-null assertion is safe due to regex match const name = (alias ? alias + '.' : '') + functionName const functionBuild: any = { @@ -189,6 +189,17 @@ export class PineParser { args: [], originalName: functionName, body: body, + doc: docstring, // Store the captured docstring + kind: 'User Function', // Add a specific kind for user-defined functions + } + + if (exportKeyword) { + functionBuild.export = true + functionBuild.kind = 'User Export Function' // More specific kind + } + if (methodKeyword) { + functionBuild.method = true + functionBuild.kind = 'User Method' // More specific kind } const funcParamsMatches = parameters.matchAll(this.funcArgPattern) @@ -216,6 +227,33 @@ export class PineParser { } functionBuild.args.push(argsDict) } + // Parse @param descriptions from docstring + if (docstring) { + const lines = docstring.split('\n') + interface ParsedFunctionArgument { + name: string + required: boolean + default?: string + type?: string + modifier?: string + desc?: string + } + functionBuild.args.forEach((arg: ParsedFunctionArgument) => { + if (arg.name) { + const paramLineRegex: RegExp = new RegExp( + `^\\s*\\/\\/\\s*@param\\s+${arg.name}\\s*(?:\\([^)]*\\))?\\s*(.+)`, + 'i', + ) + for (const line of lines) { + const match: RegExpMatchArray | null = line.match(paramLineRegex) + if (match && match[1]) { + arg.desc = match[1].trim() + break + } + } + } + }) + } parsedFunctions.push(functionBuild) } if (alias) { @@ -248,7 +286,7 @@ export class PineParser { const typeMatches = script.matchAll(this.typePattern) for (const typeMatch of typeMatches) { - const { typeName, fieldsGroup } = typeMatch.groups! // Non-null assertion is safe due to regex match + const { annotationsGroup, udtExportKeyword, typeName, fieldsGroup } = typeMatch.groups! const name = (alias ? alias + '.' : '') + typeName @@ -256,6 +294,13 @@ export class PineParser { name: name, fields: [], originalName: typeName, + kind: 'User Type', // Assign kind + doc: annotationsGroup || '', // Store docstring + } + + if (udtExportKeyword) { + typeBuild.export = true + typeBuild.kind = 'User Export Type' // More specific kind } if (fieldsGroup) { @@ -263,6 +308,7 @@ export class PineParser { for (const fieldMatch of fieldMatches) { const { genericTypes, + isConst, // New capture group genericType1, genericType2, fieldType, @@ -273,7 +319,7 @@ export class PineParser { defaultValueNumber, defaultValueColor, defaultValueIdentifier, - } = fieldMatch.groups! // Non-null assertion is safe due to regex match + } = fieldMatch.groups! let resolvedFieldType = genericTypes ? `${fieldMatch[1] /* array|matrix|map */}<${genericType1 || ''}${ @@ -281,6 +327,10 @@ export class PineParser { }${genericType2 || ''}>` : fieldType + (isArray || '') + // Prepend 'const ' if isConst is captured + // resolvedFieldType = isConst ? `const ${resolvedFieldType}` : resolvedFieldType; + // No, we store isConst separately. Type itself remains 'string', not 'const string'. + const fieldValue = defaultValueSingleQuote || defaultValueDoubleQuote || @@ -291,10 +341,16 @@ export class PineParser { const fieldsDict: Record = { name: fieldName, type: resolvedFieldType, + kind: 'Field', + } + if (isConst) { + fieldsDict.isConst = true } if (fieldValue) { fieldsDict.default = fieldValue } + // TODO: Parse @field annotations from annotationsGroup for this specific fieldName + // and add to fieldsDict.desc typeBuild.fields.push(fieldsDict) } } diff --git a/src/PineSignatureHelpProvider.ts b/src/PineSignatureHelpProvider.ts index b457dc9..c60ddc1 100644 --- a/src/PineSignatureHelpProvider.ts +++ b/src/PineSignatureHelpProvider.ts @@ -28,6 +28,7 @@ import { Helpers, PineSharedCompletionState } from './index' import { Class } from './PineClass' import * as vscode from 'vscode' import { PineDocsManager } from './PineDocsManager' +// PineCompletionService is accessed via Class.pineCompletionService interface CompletionItem { name: string @@ -62,6 +63,7 @@ export class PineSignatureHelpProvider implements vscode.SignatureHelpProvider { private newFunction: boolean = false private keyValueMatchesSave: any = null private lastSelection: string | null = null + // private pineCompletionService: PineCompletionService // Removed private docsToMatchArgumentCompletions?: Map = Class.PineDocsManager.getMap( 'variables', 'constants', @@ -69,6 +71,13 @@ export class PineSignatureHelpProvider implements vscode.SignatureHelpProvider { 'types', ) + constructor() { + // Constructor is now empty or can be used for other initializations + // if (!Class.pineCompletionService) { + // console.error("PineCompletionService not initialized in Class object!"); + // } + } + /** * Provides signature help for a Pine function. * @param document - The current document. @@ -104,56 +113,79 @@ export class PineSignatureHelpProvider implements vscode.SignatureHelpProvider { const udtMap = Class.PineDocsManager.getMap('UDT', 'types') if (udtMap.has(udtName)) { isUdtNew = true - const udtFunctionName = udtName + const udtFunctionName = udtName } else { udtName = null } } if (isUdtNew && udtName) { - const udtMap = Class.PineDocsManager.getMap('UDT', 'types') - const udtDocs = udtMap.get(udtName) + // const udtMap = Class.PineDocsManager.getMap('UDT', 'types') + // const udtDocs = udtMap.get(udtName) + const udtDocs = Class.pineCompletionService.getFunctionDocs(udtName + '.new') - if (udtDocs && udtDocs.fields) { + if (udtDocs && udtDocs.args) { // Expect 'args' from service instead of 'fields' const fieldCompletions: Record = {} const fieldArgsForState: CompletionItem[] = [] - const fieldNames: string[] = [] + // const fieldNames: string[] = [] // fieldNames will be derived from udtDocs.args - udtDocs.fields.forEach((field: any, index: number) => { + // The service returns args with 'name', 'type', 'desc', 'default', 'required' + const fieldNames = udtDocs.args.map((arg: any) => arg.name) + + udtDocs.args.forEach((field: any, index: number) => { const completionItem: CompletionItem = { - name: `${field.name}=`, - kind: 'Field', + name: `${field.name}=`, // Service should provide name directly + kind: 'Field', // Service provides kind, but here we know it's a field for .new() desc: field.desc || `Field ${field.name} of type ${field.type}.`, - defaultValue: field.defaultValue, - required: !field.defaultValue, + defaultValue: field.default, // Service provides default + required: field.required, // Service provides required preselect: index === 0, } fieldArgsForState.push(completionItem) - fieldNames.push(field.name) + // fieldNames.push(field.name); // Already handled fieldCompletions[field.name] = [completionItem] }) - PineSharedCompletionState.setCompletions(fieldCompletions) PineSharedCompletionState.setArgs(fieldNames) - PineSharedCompletionState.setActiveArg(fieldNames[0] ?? '0') - interface UdtField { - name: string - type: string - desc?: string - defaultValue?: any - } - interface UdtDocs { - fields: UdtField[] + const signatureLabel = `${udtName}.new(${udtDocs.args + .map((f: any) => `${f.name}: ${f.type}${f.default ? ' = ...' : ''}`) + .join(', ')})` + const udtSignature: vscode.SignatureInformation = new vscode.SignatureInformation(signatureLabel) + + if (udtDocs.desc) { // UDT's main description, if available from service + udtSignature.documentation = new vscode.MarkdownString(udtDocs.desc) } - const udtDocsTyped = udtDocs as UdtDocs - const udtSignature: vscode.SignatureInformation = new vscode.SignatureInformation( - `${udtName}.new(${udtDocsTyped.fields.map((f: UdtField) => `${f.name}: ${f.type}`).join(', ')})`, - ) - udtSignature.parameters = udtDocs.fields.map( - (f: any) => new vscode.ParameterInformation(`${f.name}: ${f.type}`, f.desc), - ) + + udtSignature.parameters = udtDocs.args.map((field: any) => { + const paramLabel = `${field.name}: ${field.type}` + let docString = new vscode.MarkdownString() + docString.appendCodeblock(`(field) ${paramLabel}`, 'pine') + if (field.desc) { + docString.appendMarkdown(`\n\n${field.desc}`) + } + // The old logic for extracting @field from udtDocs.doc might be less relevant + // if the service's getFunctionDocs for .new already processes fields into args with descriptions. + return new vscode.ParameterInformation(paramLabel, docString) + }) this.signatureHelp.signatures.push(udtSignature) + this.paramIndexes = [fieldNames] + this.activeSignature = 0 + + this.signatureHelp.activeParameter = this.calculateActiveParameter() + PineSharedCompletionState.setActiveParameterNumber(this.signatureHelp.activeParameter) + + const activeFieldDoc = udtDocs.args[this.signatureHelp.activeParameter] + if (activeFieldDoc) { + const simplifiedDocsForField = { + args: udtDocs.args, // Pass all fields (as args) for context + name: udtName + '.new', + } + const udtActiveSignatureHelper = udtDocs.args.map((f: any) => ({ arg: f.name, type: f.type })) + await this.sendCompletions(simplifiedDocsForField, udtActiveSignatureHelper) + } + + await this.setActiveArg(this.signatureHelp) // --- DEBUG LOGS --- // console.log('UDT Name:', udtName) @@ -177,17 +209,22 @@ export class PineSignatureHelpProvider implements vscode.SignatureHelpProvider { return null } - const { isMethod, map } = this.determineFunctionType(functionName) - if (!map) { - return null - } + // const { isMethod, map } = this.determineFunctionType(functionName) + // if (!map) { + // return null + // } + // const docs = map.get(functionName) + + const docs = Class.pineCompletionService.getFunctionDocs(functionName) - const docs = map.get(functionName) if (!docs) { return null } - const methodString = isMethod ? this.extractMethodString(docs) : null + const isMethod = docs.kind === 'Method' // Determine if it's a method from the service response + // const methodString = isMethod ? this.extractMethodString(docs) : null // extractMethodString might be obsolete if service provides full name + const methodString = isMethod ? functionName : null // Use functionName if it's a method (e.g. namespace.methodName) + const [buildSignatures, activeSignatureHelper, paramIndexes] = this.buildSignatures(docs, isMethod, methodString) this.paramIndexes = paramIndexes this.signatureHelp.signatures = buildSignatures @@ -258,48 +295,48 @@ export class PineSignatureHelpProvider implements vscode.SignatureHelpProvider { return trimMatch?.[0] || null } - private determineFunctionType(functionName: string): { isMethod: boolean; map: Map | null } { - let isMethod = false - let map: Map | null = null - - const funcMap = Class.PineDocsManager.getMap('functions', 'functions2') - if (funcMap.has(functionName)) { - return { isMethod, map: funcMap } - } - - const methodMap = Class.PineDocsManager.getMap('methods', 'methods2') - for (const key of methodMap.keys()) { - const keySplit = key.includes('.') ? key.split('.')[1] : key - const [namespace, methodName] = functionName.split('.') - - if (keySplit === methodName) { - const type = Helpers.identifyType(namespace) - const docs = methodMap.get(key) - - if ( - !type || - !docs || - (typeof type === 'string' && !docs.thisType.includes(Helpers.replaceType(type).replace(/<[^>]+>|\[\]/g, ''))) - ) { - continue - } - - isMethod = true - return { isMethod, map: methodMap } - } - } - - return { isMethod, map } - } - - private extractMethodString(docs: PineDocsManager): string | null { - const trimMatch = this.line.slice(0, this.line.lastIndexOf('(', this.position.character)).match(/([\w.]+)$/g) - const methodString = trimMatch?.[0] || null - return methodString && docs.thisType ? methodString : null - } + // private determineFunctionType(functionName: string): { isMethod: boolean; map: Map | null } { + // let isMethod = false + // let map: Map | null = null + + // const funcMap = Class.PineDocsManager.getMap('functions', 'functions2') + // if (funcMap.has(functionName)) { + // return { isMethod, map: funcMap } + // } + + // const methodMap = Class.PineDocsManager.getMap('methods', 'methods2') + // for (const key of methodMap.keys()) { + // const keySplit = key.includes('.') ? key.split('.')[1] : key + // const [namespace, methodName] = functionName.split('.') + + // if (keySplit === methodName) { + // const type = Helpers.identifyType(namespace) + // const docs = methodMap.get(key) + + // if ( + // !type || + // !docs || + // (typeof type === 'string' && !docs.thisType.includes(Helpers.replaceType(type).replace(/<[^>]+>|\[\]/g, ''))) + // ) { + // continue + // } + + // isMethod = true + // return { isMethod, map: methodMap } + // } + // } + + // return { isMethod, map } + // } + + // private extractMethodString(docs: PineDocsManager): string | null { + // const trimMatch = this.line.slice(0, this.line.lastIndexOf('(', this.position.character)).match(/([\w.]+)$/g) + // const methodString = trimMatch?.[0] || null + // return methodString && docs.thisType ? methodString : null + // } private buildSignatures( - docs: PineDocsManager, + docs: any, // Changed from PineDocsManager to any to accept service response isMethod: boolean = false, methodString: string | null = null, ): [vscode.SignatureInformation[], Record[][], string[][]] { @@ -669,8 +706,63 @@ export class PineSignatureHelpProvider implements vscode.SignatureHelpProvider { completions.push(...paramArray) } + // Add literal suggestions for primitive types if no specific values were found or to augment them if (docs) { + // docs here is argDocs for the current parameter/field + const currentArgPrimaryType = (this.getArgTypes(docs)?.[0] || '').toLowerCase() // Get primary type like 'string', 'bool', 'int', 'float' + + switch (currentArgPrimaryType) { + case 'string': + if (!completions.some((c) => c.name === '""' || c.kind === 'Literal String')) { + // Avoid adding if already suggested (e.g. as a default) + completions.push({ + name: '""', + kind: 'Literal String', + desc: 'Empty string literal.', + type: 'string', + default: false, + }) + } + break + case 'bool': + if (!completions.some((c) => c.name === 'true')) { + completions.push({ name: 'true', kind: 'Boolean', desc: 'Boolean true.', type: 'bool', default: false }) + } + if (!completions.some((c) => c.name === 'false')) { + completions.push({ name: 'false', kind: 'Boolean', desc: 'Boolean false.', type: 'bool', default: false }) + } + break + case 'int': + case 'float': + // Check if '0' or a variant is already present from variable suggestions or default value + const hasNumericZeroEquivalent = completions.some( + (c) => c.name === '0' || c.name === '0.0' || c.name === 'na', + ) + if (!hasNumericZeroEquivalent) { + completions.push({ + name: '0', + kind: 'Value', + desc: `Number zero.`, + type: currentArgPrimaryType, + default: false, + }) + } + // Suggest 'na' for numeric types if not already present (often used as a default/nil value in Pine) + if (!completions.some((c) => c.name === 'na')) { + completions.push({ + name: 'na', + kind: 'Value', + desc: 'Not a number value.', + type: currentArgPrimaryType, + default: false, + }) + } + break + } + + // Existing logic for suggesting variables of matching types const argTypes = this.getArgTypes(docs) + // Reuse cached argTypes instead of re-calling this.getArgTypes(docs) const maps = [ Class.PineDocsManager.getMap('fields2'), Class.PineDocsManager.getMap('variables2'), diff --git a/src/PineTypify.ts b/src/PineTypify.ts index 312fcba..858390a 100644 --- a/src/PineTypify.ts +++ b/src/PineTypify.ts @@ -108,10 +108,159 @@ export class PineTypify { ]), ) + // Add built-in boolean constants + this.typeMap.set('true', { baseType: 'bool' }) + this.typeMap.set('false', { baseType: 'bool' }) + + // Add 'na' as float + this.typeMap.set('na', { baseType: 'float' }) + + // Add common built-in color constants + const commonColors = [ + 'aqua', + 'black', + 'blue', + 'fuchsia', + 'gray', + 'green', + 'lime', + 'maroon', + 'navy', + 'olive', + 'orange', + 'purple', + 'red', + 'silver', + 'teal', + 'white', + 'yellow', + ] + commonColors.forEach((color) => { + this.typeMap.set(`color.${color}`, { baseType: 'color' }) + }) + + // Example for UDTs (if any were predefined or commonly used and not in docs) + // this.typeMap.set('myCustomUDT', { baseType: 'myCustomUDT', isUDT: true }); + // Fetch and parse UDT definitions (placeholder - requires actual UDT definitions) // const udtDefinitions = await this.fetchUDTDefinitions(); // this.parseAndAddUDTs(udtDefinitions); } + + private inferTypeFromValue(valueString: string, variableName: string): ParsedType | null { + valueString = valueString.trim() + + // 1. String Literals + if ( + (valueString.startsWith('"') && valueString.endsWith('"')) || + (valueString.startsWith("'") && valueString.endsWith("'")) + ) { + return { baseType: 'string' } + } + if (/^str\.format\s*\(/.test(valueString)) { + return { baseType: 'string' } + } + + // 2. Boolean Literals + if (valueString === 'true' || valueString === 'false') { + return { baseType: 'bool' } + } + + // 3. 'na' Value - check before numbers as 'na' is not a number + if (valueString === 'na') { + return { baseType: 'float' } // Default 'na' to float + } + + // 4. Number Literals + if (/^-?\d+$/.test(valueString)) { + // Integer + return { baseType: 'int' } + } + if (/^-?(?:\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?$/.test(valueString) || /^-?\d+[eE][-+]?\d+$/.test(valueString)) { + // Float + return { baseType: 'float' } + } + + // 5. Color Literals & Functions + if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/.test(valueString)) { + return { baseType: 'color' } + } + if (/^color\.(new|rgb)\s*\(/.test(valueString)) { + // covers color.new(...) and color.rgb(...) + return { baseType: 'color' } + } + // Check for known color constants from typeMap (e.g., color.red) + const knownColor = this.typeMap.get(valueString) + if (knownColor && knownColor.baseType === 'color') { + return { baseType: 'color' } + } + + // 6. Ternary Expressions + // Improved regex to better handle nested ternaries or complex conditions by focusing on the last '?' + // This is a common way to parse right-associative operators. + // It tries to find the main ? : operators for the current expression level. + let openParen = 0 + let questionMarkIndex = -1 + let colonIndex = -1 + + for (let i = 0; i < valueString.length; i++) { + if (valueString[i] === '(') openParen++ + else if (valueString[i] === ')') openParen-- + else if (valueString[i] === '?' && openParen === 0 && questionMarkIndex === -1) { + questionMarkIndex = i + } else if (valueString[i] === ':' && openParen === 0 && questionMarkIndex !== -1) { + colonIndex = i + break // Found the main ternary operator for this level + } + } + + if (questionMarkIndex !== -1 && colonIndex !== -1) { + // const conditionStr = valueString.substring(0, questionMarkIndex).trim(); + const expr1String = valueString.substring(questionMarkIndex + 1, colonIndex).trim() + const expr2String = valueString.substring(colonIndex + 1).trim() + + const type1 = this.inferTypeFromValue(expr1String, '') + const type2 = this.inferTypeFromValue(expr2String, '') + + if (type1 && type2) { + if (type1.baseType === type2.baseType) { + return type1 + } + // Pine script specific coercions if known, e.g. int + float = float + if ( + (type1.baseType === 'float' && type2.baseType === 'int') || + (type1.baseType === 'int' && type2.baseType === 'float') + ) { + return { baseType: 'float' } + } + // if one is 'na' (which infers to float by default by this function) and the other is a concrete type, prefer the concrete type. + if (type1.baseType === 'float' && expr1String === 'na' && type2.baseType !== 'float') return type2 + if (type2.baseType === 'float' && expr2String === 'na' && type1.baseType !== 'float') return type1 + // If both are 'na' or both are float (one might be 'na') + if (type1.baseType === 'float' && type2.baseType === 'float') return { baseType: 'float' } + // If types are different and not 'na' involved in a special way, it's ambiguous or requires specific pine coercion rules. + // For now, could return null or a preferred type if one exists (e.g. float is often a safe bet for numbers) + // Returning null means we don't type it if ambiguous. + return null + } else if (type1 && expr2String === 'na') { + // expr2 is 'na' + return type1 + } else if (type2 && expr1String === 'na') { + // expr1 is 'na' + return type2 + } + return null // Could not determine a definitive type for the ternary + } + + // Fallback: Check typeMap for the value itself (e.g. if 'high' (a float variable) is assigned) + // This means the RHS is a known variable. + const directKnownType = this.typeMap.get(valueString) + if (directKnownType) { + return directKnownType + } + + return null // Cannot infer type + } /** * Fetches UDT definitions from the current project or external libraries. * @returns A string containing UDT definitions. @@ -149,47 +298,120 @@ export class PineTypify { } const text = document.getText() - let edits: vscode.TextEdit[] = [] + const edits: vscode.TextEdit[] = [] + const lines = text.split(/\r?\n/) + const processedLines = new Set() // To avoid processing a line multiple times if old and new logic overlap - this.typeMap.forEach((type, name) => { - const regex = new RegExp( - `(?|\\?))(?!.*,\\s*\\n)`, - 'g', - ) - let match - while ((match = regex.exec(text)) !== null) { - if (!type.baseType || /(plot|hline|undetermined type)/g.test(type.baseType)) { - continue - } + // Phase 1: Inference for Untyped Declarations + // Regex to find lines like: `[var[ip]] variableName = valueOrExpression` + // It avoids lines that already start with a type keyword, common function keywords, or comments. + // It captures: 1=var/varip (optional), 2=variable name, 3=value part + const untypedVarRegex = + /^\s*(?!pine|import|export|plotchar|plotshape|plotarrow|plot|fill|hline|strategy|indicator|alertcondition|type|fun_declaration|method|if|for|while|switch|bgcolor|plotcandle|plotbar|alert|log)\s*(?:(var\s+|varip\s+)\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*([^;\n]+(?:\n\s*\?\s*[^;\n]+:\s*[^;\n]+)?)(?:;|$)/gm - const lineStartIndex = text.lastIndexOf('\n', match.index) + 1 - const lineEndIndex = text.indexOf('\n', match.index) - const range = new vscode.Range( - document.positionAt(lineStartIndex), - document.positionAt(lineEndIndex !== -1 ? lineEndIndex : text.length), - ) + for (let i = 0; i < lines.length; i++) { + if (processedLines.has(i) || lines[i].trim().startsWith('//')) { + continue + } - if (edits.some((edit) => range.intersection(edit.range))) { - continue - } + // Check if line already has a type (simple check, can be more robust) + // Example: float myVar = ..., string x = "..." + if ( + /^\s*(?:float|int|bool|string|color|array|matrix|map|box|line|label|table|defval)\s+[a-zA-Z_]/.test(lines[i]) + ) { + continue + } - const lineText = text.substring(lineStartIndex, lineEndIndex !== -1 ? lineEndIndex : text.length) - if (lineText.startsWith('//')) { - continue - } + // Test the specific regex for untyped variables on the current line + // We need to adjust the regex to work per line or use matchAll on the whole text and map to lines. + // For simplicity, let's process line by line with a modified regex. + const lineUntypedVarRegex = + /^\s*(?:(var\s+|varip\s+)\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*([^;\n]+(?:\n\s*\?\s*[^;\n]+:\s*[^;\n]+)?)(?:;|$)/ + const match = lines[i].match(lineUntypedVarRegex) - if (RegExp(`\\b(${this.stringifyParsedType(type)}|\\s*\\[\\])\\s+${name}\\b`, 'g').test(lineText)) { - continue - } + if (match) { + const varIpPart = match[1] || '' // "var " or "varip " or "" + const variableName = match[2] + const valueExpression = match[3].trim() + + // Skip if it's a re-assignment, not a declaration (heuristic: check if var/varip is used or if it's inside a block without var/varip) + // This needs more sophisticated scope analysis, but for now, if no var/varip, assume re-assignment unless it's global scope (hard to tell without parser) + // A simple heuristic: if not var/varip, and not at global indent level (0), it's likely a re-assignment. + // However, the problem asks to type declarations. `a = 1` at global scope is a declaration. - const replacementText = lineText - .replace(new RegExp(`(? { + // ... (old logic) ... + // Ensure to check `processedLines.has(lineNumber)` if this is re-enabled. + }); + */ + + if (edits.length > 0) { + await EditorUtils.applyEditsToDocument(edits) + } } /** diff --git a/src/extension.ts b/src/extension.ts index 67d2062..6810e7c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { PineTypify } from './index' import { PineLint } from './PineLint' import { checkForNewVersionAndShowChangelog } from './newVersionPopUp' import * as vscode from 'vscode' +import { PineCompletionService } from './PineCompletionService' export function deactivate() { PineLint.versionClear() @@ -37,13 +38,21 @@ export async function activate(context: vscode.ExtensionContext) { // Set context VSCode.setContext(context) Class.setContext(context) + + // Initialize PineDocsManager and PineCompletionService + // PineDocsManager is accessed via a getter that initializes it if not already. + // We need to ensure PineDocsManager is ready before PineCompletionService uses it. + // Accessing the getter ensures it's initialized. + const docmanager = Class.PineDocsManager // Ensure PineDocsManager is initialized + Class.pineCompletionService = new PineCompletionService(docmanager) + PineLint.initialLint() // Push subscriptions to context context.subscriptions.push( PineLint.DiagnosticCollection, vscode.window.onDidChangeActiveTextEditor(async () => { - Class.PineDocsManager.cleanDocs() + docmanager.cleanDocs() PineResponseFlow.resetDocChange() if (VSCode.LanguageId !== 'pine' && !VSCode.ActivePineFile) { deactivate() @@ -55,19 +64,32 @@ export async function activate(context: vscode.ExtensionContext) { PineLint.initialLint() } }), - VSCode.Wspace.onDidOpenTextDocument(async () => { + vscode.workspace.onDidOpenTextDocument(async () => { if (VSCode.ActivePineFile) { PineLint.handleDocumentChange() } }), - VSCode.Wspace.onDidChangeTextDocument(async (event) => { + vscode.workspace.onDidChangeTextDocument(async (event) => { if (event.contentChanges.length > 0 && VSCode.ActivePineFile) { PineLint.handleDocumentChange() timerStart = new Date().getTime() } }), + vscode.workspace.onDidChangeConfiguration(() => { + console.log('Configuration changed') + }), + + vscode.workspace.onDidCloseTextDocument((document) => { + console.log('Document closed:', document.fileName) + PineLint.handleDocumentChange() + }), + + vscode.workspace.onDidSaveTextDocument((document) => { + console.log('Document saved:', document.fileName) + }), + VSCode.RegisterCommand('pine.docString', async () => new PineDocString().docstring()), VSCode.RegisterCommand('pine.getStandardList', async () => Class.PineScriptList.showMenu('built-in')), VSCode.RegisterCommand('pine.typify', async () => new PineTypify().typifyDocument()), @@ -76,21 +98,36 @@ export async function activate(context: vscode.ExtensionContext) { VSCode.RegisterCommand('pine.getLibraryTemplate', async () => Class.PineTemplates.getLibraryTemplate()), VSCode.RegisterCommand('pine.setUsername', async () => Class.PineUserInputs.setUsername()), VSCode.RegisterCommand('pine.completionAccepted', () => Class.PineCompletionProvider.completionAccepted()), - VSCode.Lang.registerColorProvider({ language: 'pine', scheme: 'file' }, Class.PineColorProvider), - VSCode.Lang.registerHoverProvider({ language: 'pine', scheme: 'file' }, Class.PineHoverProvider), - VSCode.Lang.registerHoverProvider({ language: 'pine', scheme: 'file' }, Class.PineLibHoverProvider), - VSCode.Lang.registerRenameProvider('pine', Class.PineRenameProvider), - VSCode.Lang.registerInlineCompletionItemProvider('pine', Class.PineInlineCompletionContext), - VSCode.Lang.registerSignatureHelpProvider('pine', Class.PineSignatureHelpProvider, '(', ',', ' '), - VSCode.Lang.registerCompletionItemProvider('pine', Class.PineLibCompletionProvider), - VSCode.Lang.registerCompletionItemProvider('pine', Class.PineCompletionProvider, '.', ',', '('), - // VSCode.RegisterCommand('pine.startProfiler', () => {console.profile('Start of Start Profiler (Command Triggered')}), - // VSCode.RegisterCommand('pine.stopProfiler', () => {console.profileEnd('End of Start Profiler (Command Triggered')}), - // VSCode.RegisterCommand('pine.getSavedList', async () => Class.PineScriptList.showMenu('saved')), - // VSCode.RegisterCommand('pine.saveToTv', async () => { await Class.PineSaveToTradingView() } ), - // VSCode.RegisterCommand('pine.compareWithOldVersion', async () => Class.PineScriptList.compareWithOldVersion()), - // VSCode.RegisterCommand('pine.setSessionId', async () => Class.pineUserInputs.setSessionId()), - // VSCode.RegisterCommand('pine.clearKEYS', async () => Class.PineUserInputs.clearAllInfo()), + VSCode.Lang.registerColorProvider({ scheme: 'file', language: 'pine' }, Class.PineColorProvider), + VSCode.Lang.registerHoverProvider({ scheme: 'file', language: 'pine' }, Class.PineHoverProvider), + VSCode.Lang.registerHoverProvider({ scheme: 'file', language: 'pine' }, Class.PineLibHoverProvider), + VSCode.Lang.registerRenameProvider({ scheme: 'file', language: 'pine' }, Class.PineRenameProvider), + VSCode.Lang.registerInlineCompletionItemProvider( + { scheme: 'file', language: 'pine' }, + Class.PineInlineCompletionContext, + ), + VSCode.Lang.registerSignatureHelpProvider( + { scheme: 'file', language: 'pine' }, + Class.PineSignatureHelpProvider, + '(', + ',', + '', + ), + VSCode.Lang.registerCompletionItemProvider({ scheme: 'file', language: 'pine' }, Class.PineLibCompletionProvider), + VSCode.Lang.registerCompletionItemProvider( + { scheme: 'file', language: 'pine' }, + Class.PineCompletionProvider, + '.', + ',', + '(', + ), + // VSCode.RegisterCommand ('pine.startProfiler' , () => {console.profile('Start of Start Profiler (Command Triggered')}) , + // VSCode.RegisterCommand ('pine.stopProfiler' , () => {console.profileEnd('End of Start Profiler (Command Triggered')}), + // VSCode.RegisterCommand ('pine.getSavedList' , async () => Class.PineScriptList.showMenu('saved')) , + // VSCode.RegisterCommand ('pine.saveToTv' , async () => { await Class.PineSaveToTradingView() } ) , + // VSCode.RegisterCommand ('pine.compareWithOldVersion', async () => Class.PineScriptList.compareWithOldVersion()) , + // VSCode.RegisterCommand ('pine.setSessionId' , async () => Class.pineUserInputs.setSessionId()) , + // VSCode.RegisterCommand ('pine.clearKEYS' , async () => Class.PineUserInputs.clearAllInfo()) , vscode.commands.registerCommand('extension.forceLint', async () => { const response = await Class.PineRequest.lint() diff --git a/syntaxes/pine.tmLanguage.example.ps b/syntaxes/pine.tmLanguage.example.ps index 8b7b7d6..0209d59 100644 Binary files a/syntaxes/pine.tmLanguage.example.ps and b/syntaxes/pine.tmLanguage.example.ps differ diff --git a/themes/sytax-types.pine b/themes/sytax-types.pine index 41b3756..6a02c24 100644 --- a/themes/sytax-types.pine +++ b/themes/sytax-types.pine @@ -25,6 +25,7 @@ export enum ExportedEnum VALUE1 = "val 1" VALUE2 = "val 2" + // ============================== end imagonary lib area @@ -36,11 +37,11 @@ export enum ExportedEnum // ============================================================================= // @type MyUDT demonstrates all situations where we can use the int/float/string types for defaults, array/matrix/map are not allowed to have any -// @field myIntField int,defval = 0 -// @field myFloatField float,defval = 0.0 +// @field myIntField int ,defval = 0 +// @field myFloatField float ,defval = 0.0 // @field myStringField string,defval = "hello" -// @field myBoolField bool,defval = false -// @field myColorField color,defval = color.red +// @field myBoolField bool ,defval = false +// @field myColorField color ,defval = color.red type MyUDT int myIntField = 0 float myFloatField = 0.0 @@ -52,6 +53,7 @@ type MyUDT map myMapField // no default value for maps + // ============================================================================= // Library type declarations (Imaginary - For Demonstration Purposes) // ============================================================================= @@ -74,7 +76,7 @@ string myString = "Pine Script" color myColor = color.red // Series Variable Declaration -series int mySeriesInt = close +series int mySeriesInt = 100 series float mySeriesFloat = close series bool mySeriesBool = close > open series string mySeriesString = syminfo.ticker @@ -105,8 +107,8 @@ var color myVarColor = color.orange // varip Variable Declaration (persists across calculations) varip int myVaripInt = 50, myVaripInt := nz(myVaripInt,50) varip float myVaripFloat = 15.7, myVaripFloat := nz(myVaripFloat,15.7) -varip bool myVaripBool = true, myVaripBool := nz(myVaripBool,true) -varip string myVaripString = "Varip Value", myVaripString := nz(myVaripString,"Varip Value") +varip bool myVaripBool = true, myVaripBool := myVaripBool +varip string myVaripString = "Varip Value", myVaripString := myVaripString // ============================================================================= // Built-in Object Declarations @@ -115,27 +117,22 @@ varip string myVaripString = "Varip Value", myVaripString := nz(myVaripString,"V // Line Declaration line myLine = line.new(bar_index[10], low[10], bar_index, high) var line myVarLine = line.new(bar_index[5], high[5], bar_index, low) -varip line myVaripLine = line.new(bar_index[2], close[2], bar_index, open) // Label Declaration label myLabel = label.new(bar_index, high, "Label Text") var label myVarLabel = label.new(bar_index, low, "Var Label") -varip label myVaripLabel = label.new(bar_index, close, "Varip Label") // Box Declaration box myBox = box.new(bar_index[10], high[10], bar_index, low) var box myVarBox = box.new(bar_index[5], high[5], bar_index, low) -varip box myVaripBox = box.new(bar_index[2], close[2], bar_index, open) // Table Declaration table myTable = table.new(position.top_right, 2, 2) var table myVarTable = table.new(position.bottom_left, 3, 3) -varip table myVaripTable = table.new(position.middle_center, 4, 4) // Linefill Declaration linefill myLinefill = linefill.new(myLine, myVarLine, color.green) var linefill myVarLinefill = linefill.new(myLine, myVarLine, color.red) -varip linefill myVaripLinefill = linefill.new(myLine, myVarLine, color.blue) // chart.point Declaration chart.point myPoint = chart.point.now(close) @@ -145,7 +142,6 @@ varip chart.point myVaripPoint = chart.point.from_time(time=time,price=open) // polyline Declaration polyline myPolyline = polyline.new(points=array.from(myPoint,chart.point.now(close)),line_color=color.fuchsia) var polyline myPolylineVar = na -varip polyline myPolylineVarip = na // ============================================================================= // Array Declarations @@ -179,27 +175,22 @@ varip array myVaripColorArray = array.from(color.red, color.green) // Array of line array myLineArray = array.new() var array myVarLineArray = array.new(2, myLine) -varip array myVaripLineArray = array.from(myLine, myVarLine) // Array of label array