From 10ac87c2ef4cf20947fc311591d655504882ded4 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 28 Sep 2024 15:44:54 -0400 Subject: [PATCH 01/43] feat: refactor symbols db --- web/src/services/completion/service.ts | 78 +++++--- web/src/services/completion/types.ts | 108 +++++++++-- web/src/services/completion/utils.ts | 178 +++++++++++++------ web/src/services/storage/db.ts | 12 +- web/src/services/storage/types/completion.ts | 56 ++++-- 5 files changed, 315 insertions(+), 117 deletions(-) diff --git a/web/src/services/completion/service.ts b/web/src/services/completion/service.ts index 9bb172ef..5bf78c91 100644 --- a/web/src/services/completion/service.ts +++ b/web/src/services/completion/service.ts @@ -1,7 +1,14 @@ import { db, keyValue } from '../storage' -import { type CompletionRecord, CompletionRecordType } from '../storage/types' -import type { GoImportsFile, SuggestionQuery } from './types' -import { buildCompletionRecord, completionRecordsFromMap, importRecordsIntoSymbols, buildTableQuery } from './utils' +import type { GoIndexFile, SuggestionQuery } from './types' +import { + completionFromPackage, + completionFromSymbol, + findPackagePathFromContext, + importCompletionFromPackage, + intoPackageIndexItem, + intoSymbolIndexItem, +} from './utils' +import { type SymbolIndexItem } from '~/services/storage/types' const completionVersionKey = 'completionItems.version' @@ -45,15 +52,43 @@ export class GoCompletionService { async getSymbolSuggestions(query: SuggestionQuery) { await this.checkCacheReady() - const { keys, values } = buildTableQuery(query) - const results = await this.db.completionItems.where(keys).equals(values).sortBy('label') - return results + if (query.packageName) { + return await this.getMemberSuggestion(query as Required) + } + + return await this.getLiteralSuggestion(query) + } + + private async getMemberSuggestion({ value, packageName, context }: Required) { + // If package with specified name is imported - filter symbols + // to avoid overlap with packages with eponymous name. + const packagePath = findPackagePathFromContext(context, packageName) + + const query: Partial = packagePath + ? { + packagePath, + } + : { packageName, prefix: value } + + const symbols = await this.db.symbolIndex.where(query).toArray() + return symbols.map((symbol) => completionFromSymbol(symbol, context, !!packagePath)) + } + + private async getLiteralSuggestion({ value, context }: SuggestionQuery) { + const packages = await this.db.packageIndex.where('prefix').equals(value).toArray() + const builtins = await this.db.symbolIndex.where('packagePath').equals('builtin').toArray() + + const packageCompletions = packages.map((item) => completionFromPackage(item, context)) + const symbolsCompletions = builtins.map((item) => completionFromSymbol(item, context, false)) + + return packageCompletions.concat(symbolsCompletions) } private async getStandardPackages() { await this.checkCacheReady() - const symbols = await this.db.completionItems.where('recordType').equals(CompletionRecordType.ImportPath).toArray() - return symbols + + const results = await this.db.packageIndex.toArray() + return results.map(importCompletionFromPackage) } private async checkCacheReady() { @@ -68,7 +103,7 @@ export class GoCompletionService { return true } - const count = await this.db.completionItems.count() + const count = await this.db.packageIndex.count() this.cachePopulated = count > 0 if (!this.cachePopulated) { await this.populateCache() @@ -77,25 +112,26 @@ export class GoCompletionService { } private async populateCache() { - const rsp = await fetch('/data/imports.json') + const rsp = await fetch('/data/go-index.json') if (!rsp.ok) { throw new Error(`${rsp.status} ${rsp.statusText}`) } - const data: GoImportsFile = await rsp.json() + const data: GoIndexFile = await rsp.json() + if (data.version > 1) { + console.warn(`unsupported symbol index version: ${data.version}, skip update.`) + return + } - // Completion options for import paths and package names are 2 separate records. - const importPaths = data.packages.map((pkg) => buildCompletionRecord(pkg, CompletionRecordType.ImportPath)) - const records: CompletionRecord[] = [ - ...importPaths, - ...completionRecordsFromMap(data.symbols), - ...importRecordsIntoSymbols(data.packages), - ] + const packages = data.packages.map(intoPackageIndexItem) + const symbols = data.symbols.map(intoSymbolIndexItem) await Promise.all([ - this.db.completionItems.clear(), - this.db.completionItems.bulkAdd(records), - this.keyValue.setItem(completionVersionKey, data.format), + this.db.packageIndex.clear(), + this.db.symbolIndex.clear(), + this.db.packageIndex.bulkAdd(packages), + this.db.symbolIndex.bulkAdd(symbols), + this.keyValue.setItem(completionVersionKey, data.go), ]) this.cachePopulated = true diff --git a/web/src/services/completion/types.ts b/web/src/services/completion/types.ts index 911d8376..88e03546 100644 --- a/web/src/services/completion/types.ts +++ b/web/src/services/completion/types.ts @@ -1,35 +1,113 @@ import type * as monaco from 'monaco-editor' -export type CompletionItems = monaco.languages.CompletionItem[] +export enum ImportClauseType { + /** + * There is no any import block. + */ + None, + + /** + * Single line import. + */ + Single, + + /** + * Multi-line import block with braces. + */ + Block, +} + +export interface SuggestionContext { + /** + * Current edit range + */ + range: monaco.IRange + + /** + * Controls how auto import suggestions will be added. + */ + imports: { + /** + * List of import paths from all import blocks. + */ + allPaths?: Set + + /** + * Imports in a last block related to `range`. + */ + blockPaths?: string[] + + /** + * Type of nearest import block. + */ + blockType: ImportClauseType + + /** + * Position of nearest import block to insert new imports. + * + * If `blockType` is `ImportClauseType.None` - points to position + * of nearest empty line after `package` clause. + * + * If there is no empty line after `package` clause - should point + * to the end of clause statement + 1 extra column. + * + * Otherwise - should point to a full range of last `import` block. + * + * @see prependNewLine + */ + range: monaco.IRange + + /** + * Indicates whether extra new line should be appended before `import` clause. + * + * Effective only when `range` is `ImportClauseType.None`. + */ + prependNewLine?: boolean + } +} export interface SuggestionQuery { packageName?: string value: string + context: SuggestionContext +} + +export interface PackageInfo { + name: string + importPath: string + documentation: string +} + +export interface SymbolInfo { + name: string + documentation: string + detail: string + insertText: string + kind: monaco.languages.CompletionItemKind + package: { + name: string + path: string + } } -/** - * Go standard packages list dumped by `pkgindexer` tool. - */ -export interface GoImportsFile { +export interface GoIndexFile { /** - * File format version + * File format version. */ - format?: string + version: number /** - * Go version used to generate list. - * - * Key kept for historical reasons. + * Go version used to generate index. */ - version: string + go: string /** - * List of go packages + * List of standard packages. */ - packages: CompletionItems + packages: PackageInfo[] /** - * Key-value pair of package name and its symbols. + * List of symbols of each package. */ - symbols: Record + symbols: SymbolInfo[] } diff --git a/web/src/services/completion/utils.ts b/web/src/services/completion/utils.ts index e136e704..42dfbed9 100644 --- a/web/src/services/completion/utils.ts +++ b/web/src/services/completion/utils.ts @@ -1,72 +1,132 @@ -import { type CompletionItem, type CompletionRecord, type IndexableKey, CompletionRecordType } from '../storage/types' -import type { SuggestionQuery } from './types' +import type * as monaco from 'monaco-editor' +import { ImportClauseType, type PackageInfo, type SuggestionContext, type SymbolInfo } from './types' +import type { PackageIndexItem, SymbolIndexItem } from '../storage/types' -const labelToString = (label: CompletionItem['label']) => (typeof label === 'string' ? label : label.label) -const getPrefix = (label: CompletionItem['label']) => labelToString(label)[0] ?? '' -const pkgNameFromImportPath = (importPath: string): string => { - const slashPos = importPath.lastIndexOf('/') - return slashPos === -1 ? importPath : importPath.slice(slashPos + 1) -} +type CompletionItem = monaco.languages.CompletionItem + +const getPrefix = (str: string) => str[0]?.toLowerCase() ?? '' -export const buildCompletionRecord = ( - src: CompletionItem, - recordType: CompletionRecordType, - pkgName: string = '', -): CompletionRecord => ({ - ...src, - recordType, - prefix: recordType === CompletionRecordType.Symbol ? getPrefix(src.label) : '', - packageName: pkgName, +// Although monaco doesn't require actual range, it's defined as required in TS types. +// This is a stub value to satisfy type checks. +const stubRange = undefined as any as monaco.IRange + +const packageCompletionKind = 8 + +export const intoPackageIndexItem = ({ name, importPath, documentation }: PackageInfo): PackageIndexItem => ({ + importPath, + name, + prefix: getPrefix(name), + documentation: { + value: documentation, + isTrusted: true, + }, }) -/** - * Converts import path suggestions into package name suggestions. - */ -export const importRecordsIntoSymbols = (src: CompletionItem[]): CompletionRecord[] => - src.map(({ label, kind, documentation }) => { - const importPath = labelToString(label) - const pkgName = pkgNameFromImportPath(importPath) - - return buildCompletionRecord( - { - label: pkgName, - detail: pkgName, - kind, - documentation, - insertText: pkgName, - } as any as CompletionItem, - CompletionRecordType.Symbol, - ) - }) - -export const completionRecordsFromMap = (m?: Record): CompletionRecord[] => { - if (!m) { - return [] +export const intoSymbolIndexItem = ({ + name, + package: pkg, + documentation, + ...completion +}: SymbolInfo): SymbolIndexItem => ({ + ...completion, + key: `${pkg.path}.${name}`, + prefix: getPrefix(name), + label: name, + packageName: pkg.name, + packagePath: pkg.path, + documentation: { + value: documentation, + isTrusted: true, + }, +}) + +export const importCompletionFromPackage = ({ importPath, name, documentation }: PackageIndexItem): CompletionItem => ({ + label: importPath, + documentation, + detail: name, + insertText: importPath, + kind: packageCompletionKind, + range: stubRange, +}) + +type ISingleEditOperation = monaco.editor.ISingleEditOperation + +const importPackageTextEdit = ( + importPath: string, + { imports }: SuggestionContext, +): ISingleEditOperation[] | undefined => { + if (imports.allPaths?.has(importPath)) { + return undefined } - return Object.entries(m) - .map(([pkgName, entries]) => - entries.map((entry) => buildCompletionRecord(entry, CompletionRecordType.Symbol, pkgName)), - ) - .flat() -} + switch (imports.blockType) { + case ImportClauseType.None: { + const text = `import "${importPath}"\n` + return [ + { + text: imports.prependNewLine ? `\n${text}` : text, + range: imports.range, + }, + ] + } + case ImportClauseType.Single: + case ImportClauseType.Block: { + const importLines = (imports.blockPaths ?? []) + .concat(importPath) + .sort() + .map((v) => `\t"${v}"`) + .join('\n') -type ValuesForKeys = { - [I in keyof K]: K[I] extends IndexableKey ? CompletionRecord[K[I]] : never + return [ + { + text: `import (\n${importLines}\n)`, + range: imports.range, + }, + ] + } + } } -interface CompoundIndexQuery { - keys: K - values: ValuesForKeys -} +export const completionFromPackage = ( + { importPath, name, documentation }: PackageIndexItem, + ctx: SuggestionContext, +): CompletionItem => ({ + label: name, + documentation, + detail: importPath, + insertText: name, + kind: packageCompletionKind, + range: ctx.range, + additionalTextEdits: importPackageTextEdit(importPath, ctx), +}) -export const buildTableQuery = ({ packageName, value }: SuggestionQuery) => { - // Nullable values aren't indexable. Default is empty string. - const pkg = packageName ?? '' +export const completionFromSymbol = ( + { packagePath, ...completionItem }: SymbolIndexItem, + ctx: SuggestionContext, + textEdits: boolean, +): CompletionItem => ({ + ...completionItem, + range: ctx.range, + additionalTextEdits: textEdits ? importPackageTextEdit(packagePath, ctx) : undefined, +}) - // Enforce strict type check to avoid issues if schema of model changes. - const keys = ['recordType', 'packageName', 'prefix'] as K - const values = [CompletionRecordType.Symbol, pkg, value] as ValuesForKeys +const pkgNameFromPath = (importPath: string) => { + const slashPos = importPath.lastIndexOf('/') + return slashPos === -1 ? importPath : importPath.slice(slashPos + 1) +} + +/** + * Attempts to find first import path that matches package name. + */ +export const findPackagePathFromContext = ({ imports }: SuggestionContext, pkgName: string): string | undefined => { + if (!imports.allPaths) { + return undefined + } - return { keys, values } satisfies CompoundIndexQuery + for (const importPath of imports.allPaths.keys()) { + // TODO: support named imports + if (pkgName === pkgNameFromPath(importPath)) { + return importPath + } + } } diff --git a/web/src/services/storage/db.ts b/web/src/services/storage/db.ts index 12ac0c85..b492858c 100644 --- a/web/src/services/storage/db.ts +++ b/web/src/services/storage/db.ts @@ -1,23 +1,21 @@ import Dexie, { type Table } from 'dexie' -import type { CacheEntry, CompletionRecord } from './types' +import type { CacheEntry, PackageIndexItem, SymbolIndexItem } from './types' /** * IndexedDB-based cache implementation. */ export class DatabaseStorage extends Dexie { keyValue!: Table - completionItems!: Table + packageIndex!: Table + symbolIndex!: Table constructor() { super('CacheStore') - // Init table with 2 indexes: - // - // [recordType+packageName+prefix] - For monaco autocompletion - // [recordType+packageName+label] - For hover (codelens) this.version(2).stores({ keyValue: 'key', - completionItems: '++id,recordType,[recordType+packageName+prefix],[recordType+packageName+label]', + packageIndex: 'importPath, prefix, name', + symbolIndex: 'key, packagePath, [prefix+packageName]', }) } } diff --git a/web/src/services/storage/types/completion.ts b/web/src/services/storage/types/completion.ts index 90e87afc..1262a3a2 100644 --- a/web/src/services/storage/types/completion.ts +++ b/web/src/services/storage/types/completion.ts @@ -3,36 +3,62 @@ import type * as monaco from 'monaco-editor' export type CompletionItem = monaco.languages.CompletionItem export type CompletionItems = monaco.languages.CompletionItem[] -export enum CompletionRecordType { - None, - ImportPath, - Symbol, +/** + * Normalized version of CompletionItem that contains fixed types instead of union (e.g. Foo | Bar) + */ +export interface NormalizedCompletionItem extends Omit { + label: string + documentation: monaco.IMarkdownString } /** - * Type represents CompletionRecord keys that can be used for query using DB index. + * Represents record from package index. */ -export type IndexableKey = keyof Omit +export interface PackageIndexItem { + /** + * Full import path. + */ + importPath: string + + /** + * Package name. + */ + name: string + + /** + * Prefix for search by first letter supplied by Monaco. + */ + prefix: string + + /** + * Inherited from CompletionItem. + */ + documentation: monaco.IMarkdownString +} /** - * Entity represends completion record stored in a cache. - * - * Extends monaco type with some extra indexing information. + * Represents record from symbol index. */ -export interface CompletionRecord extends monaco.languages.CompletionItem { +export interface SymbolIndexItem extends NormalizedCompletionItem { /** - * Completion category + * Key is compound pair of package name and symbol name. + * + * E.g. `syscall/js.Value` */ - recordType: CompletionRecordType + key: string /** - * Symbol name's first letter. - * Used as monaco triggers completion provider only after first character appear. + * Prefix for search by first letter supplied by Monaco. */ prefix: string /** - * Package name to what this symbol belongs. + * Full package path to which this symbol belongs. + */ + packagePath: string + + /** + * Package name part of package path */ packageName: string } From b7b0835a394a7ee7f5d54fe66bdbfe0c23716aad Mon Sep 17 00:00:00 2001 From: x1unix Date: Sun, 29 Sep 2024 12:28:20 -0400 Subject: [PATCH 02/43] feat: provide project id --- .../features/workspace/CodeEditor/CodeEditor.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx index adf5359e..4c0f07e0 100644 --- a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx +++ b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx @@ -26,15 +26,18 @@ const ANALYZE_DEBOUNCE_TIME = 500 // ask monaco-editor/react to use our own Monaco instance. configureMonacoLoader() -const mapWorkspaceProps = ({ files, selectedFile }: WorkspaceState) => { +const mapWorkspaceProps = ({ files, selectedFile, snippet }: WorkspaceState) => { + const projectId = snippet?.id ?? '' if (!selectedFile) { return { + projectId, code: '', fileName: '', } } return { + projectId, code: files?.[selectedFile], fileName: selectedFile, } @@ -43,10 +46,11 @@ const mapWorkspaceProps = ({ files, selectedFile }: WorkspaceState) => { interface CodeEditorState { code?: string loading?: boolean + fileName: string + projectId: string } interface Props extends CodeEditorState { - fileName: string darkMode: boolean vimModeEnabled: boolean isServerEnvironment: boolean From 962faef73b3c835ae595c9084023a7e920c97a91 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sun, 29 Sep 2024 12:28:42 -0400 Subject: [PATCH 03/43] feat: build import context --- .../autocomplete/symbols/imports.ts | 417 ++++++++++++++++++ .../autocomplete/symbols/provider.ts | 25 +- web/src/services/completion/types.ts | 2 +- web/src/services/completion/utils.ts | 2 +- 4 files changed, 434 insertions(+), 12 deletions(-) create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts new file mode 100644 index 00000000..15e334af --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts @@ -0,0 +1,417 @@ +import * as monaco from 'monaco-editor' + +import { ImportClauseType, type SuggestionContext } from '~/services/completion' + +type Tokens = monaco.Token[][] + +enum GoToken { + None = '', + Comment = 'comment.go', + KeywordPackage = 'keyword.package.go', + KeywordImport = 'keyword.import.go', + Parenthesis = 'delimiter.parenthesis.go', + Ident = 'identifier.go', + String = 'string.go', +} + +class ParseError extends Error { + constructor(line: number, col: number, msg: string) { + super(`Go parse error: ${msg} (at ${line}:${col})`) + } +} + +class UnexpectedTokenError extends ParseError { + constructor(line: number, token: monaco.Token) { + super(line, token.offset, `unexpected token "${token.type}`) + } +} + +const isNotEmptyToken = ({ type }: monaco.Token) => type !== GoToken.None + +const findPackageBlock = (tokens: Tokens) => { + for (let i = 0; i < tokens.length; i++) { + const row = tokens[i] + // Go file should start with package name. + // Only whitespace or comment can be above package clause. + + // Find first non-empty token. + const token = row.find(isNotEmptyToken) + if (!token) { + // Whitespace, skip + continue + } + + switch (token.type) { + case GoToken.Comment: + // comment, skip + continue + case GoToken.KeywordPackage: + return i + default: + return -1 + } + } + + return -1 +} + +interface ImportHeader { + line: number + foundParent?: boolean + argTokens?: monaco.Token[] +} + +const findImportHeader = (offset: number, tokens: Tokens): ImportHeader | null => { + for (let i = offset; i < tokens.length; i++) { + const row = tokens[i] + const j = row.findIndex(isNotEmptyToken) + if (j === -1) { + continue + } + + const token = row[j] + switch (token.type) { + case GoToken.Comment: + continue + case GoToken.KeywordImport: + break + default: + // not a comment + return null + } + + // Block is probably multiline if next token is parentheses or empty. + const rest = row.slice(j + 1) + const k = rest.findIndex(isNotEmptyToken) + if (k === -1) { + return { line: i } + } + + switch (rest[k].type) { + case GoToken.Parenthesis: + return { line: i, foundParent: true } + case GoToken.Ident: + case GoToken.String: + // probably it's a single-line import. + return { + line: i, + argTokens: rest.slice(k), + } + default: + throw new UnexpectedTokenError(i, token) + } + } + + return null +} + +const unquote = (str: string) => { + if (!str.length) { + return str + } + + if (str[0] === '"') { + str = str.slice(1) + } + + const endPos = str.length - 1 + if (str[endPos] === '"') { + str = str.slice(0, endPos) + } + return str +} + +const readToken = (line: number, tok: monaco.Token, model: monaco.editor.ITextModel): string => { + const word = model.getWordAtPosition({ + lineNumber: line + 1, + column: tok.offset + 1, + })?.word + if (!word) { + throw new ParseError(line, tok.offset, 'parseToken: invalid range') + } + + return word +} + +const checkParenthesis = (line: number, token: monaco.Token, model: monaco.editor.ITextModel) => { + const isParent = token.type === GoToken.Parenthesis + let isClose = false + if (isParent) { + isClose = readToken(line, token, model) === ')' + } + + return { isParent, isClose } +} + +interface ImportBlock { + range: monaco.IRange + isMultiline?: boolean + imports: ImportStmt[] +} + +interface ImportStmt { + alias?: string + path: string +} + +interface ImportRow { + imports: ImportStmt[] + closeParentPos: number +} + +const readImportLine = (line: number, model: monaco.editor.ITextModel, row: monaco.Token[]): ImportStmt | null => { + const i = row.findIndex(isNotEmptyToken) + const token = row[i] + switch (token.type) { + case GoToken.Ident: { + const ident = readToken(line, token, model) + const pathTok = row.find(isNotEmptyToken) + if (!pathTok) { + throw new ParseError(line, i, 'missing import path after ident') + } + return { alias: ident, path: readToken(line, pathTok, model) } + } + case GoToken.String: + return { + path: readToken(line, token, model), + } + default: + throw new UnexpectedTokenError(line, token) + } +} + +const readImportBlockLine = (line: number, model: monaco.editor.ITextModel, row: monaco.Token[]): ImportRow | null => { + const imports: ImportStmt[] = [] + let slice = row + let lastIdent: string | null = null + while (slice.length > 0) { + const i = slice.findIndex(isNotEmptyToken) + if (i === -1) { + break + } + + const token = slice[i] + slice = slice.slice(i + 1) + const { isParent, isClose } = checkParenthesis(line, token, model) + if (isParent) { + if (lastIdent) { + throw new UnexpectedTokenError(line, token) + } + + if (isClose) { + // Group close on same line. + return { imports, closeParentPos: token.offset } + } + + throw new UnexpectedTokenError(line, token) + } + + switch (token.type) { + case GoToken.Ident: { + if (lastIdent) { + // import path expected + throw new UnexpectedTokenError(line, token) + } + + lastIdent = readToken(line, token, model) + break + } + case GoToken.Comment: { + break + } + case GoToken.String: { + const path = unquote(readToken(line, token, model)) + if (path) { + imports.push(lastIdent ? { path, alias: lastIdent } : { path }) + } + + lastIdent = null + break + } + default: + // Unexpected token + throw new UnexpectedTokenError(line, token) + } + } + + return { imports, closeParentPos: -1 } +} + +const traverseImportGroup = ( + model: monaco.editor.ITextModel, + header: ImportHeader, + tokens: Tokens, +): ImportBlock | null => { + let groupStartFound = header.foundParent ?? false + const imports: ImportStmt[] = [] + const range = { + startLineNumber: header.line, + startColumn: 1, + endLineNumber: -1, + endColumn: -1, + } + + for (let i = header.line + 1; i < tokens.length; i++) { + const row = tokens[i] + const j = row.findIndex(isNotEmptyToken) + if (j === -1) { + continue + } + + const token = row[j] + const { isParent, isClose } = checkParenthesis(i, token, model) + if (isParent) { + if (groupStartFound && isClose) { + range.endLineNumber = i + 1 + range.endColumn = token.offset + 2 + return { + range, + imports, + isMultiline: true, + } + } + + if (!groupStartFound && !isClose) { + groupStartFound = true + continue + } + + throw new UnexpectedTokenError(i, token) + } + + const r = readImportBlockLine(i, model, row) + if (!r) { + return null + } + + imports.push(...r.imports) + if (r.closeParentPos === -1) { + continue + } + + range.endLineNumber = i + 1 + range.endColumn = r.closeParentPos + 2 + return { + range, + imports, + isMultiline: true, + } + } + + throw new ParseError(header.line + 1, 1, 'unterminated import block') +} + +const findImportBlock = (offset: number, model: monaco.editor.ITextModel, tokens: Tokens): ImportBlock | null => { + const header = findImportHeader(offset, tokens) + if (!header) { + return null + } + + if (!header.argTokens) { + // multi-line + return traverseImportGroup(model, header, tokens) + } + + // single line import + const importStmt = readImportLine(header.line, model, header.argTokens) + if (!importStmt) { + // syntax error. + return null + } + + return { + range: { + startLineNumber: header.line, + endLineNumber: header.line, + startColumn: 1, + endColumn: header.argTokens[header.argTokens.length - 1].offset, + }, + imports: [importStmt], + } +} + +/** + * Gathers information about Go imports in a model and provides information necessary for auto-import for suggestions. + */ +export const buildImportContext = (model: monaco.editor.ITextModel): SuggestionContext['imports'] => { + const tokens = monaco.editor.tokenize(model.getValue(), model.getLanguageId()) + + const packagePos = findPackageBlock(tokens) + if (packagePos === -1) { + // Invalid syntax, discard any import suggestions. + return { + blockType: ImportClauseType.None, + } + } + + // Fallback insert range for imports. + const packageLine = packagePos + 1 + const packageEndCol = model.getLineLength(packageLine) + 1 + const fallbackRange: monaco.IRange = { + startLineNumber: packageLine, + endLineNumber: packageLine, + startColumn: packageEndCol, + endColumn: packageEndCol, + } + + const allImports: string[] = [] + let hasError = false + let lastImportBlock: ImportBlock | null = null + + let offset = packagePos + 1 + while (offset < tokens.length) { + try { + const block = findImportBlock(offset, model, tokens) + if (!block) { + break + } + + offset = block.range.endLineNumber + 1 + lastImportBlock = block + allImports.push(...block.imports.map(({ path }) => path)) + } catch (err) { + hasError = true + break + } + } + + if (lastImportBlock) { + // TODO: support named imports + return { + allPaths: new Set(allImports), + blockPaths: lastImportBlock.imports.map(({ path }) => path), + blockType: lastImportBlock.isMultiline ? ImportClauseType.Block : ImportClauseType.Single, + range: lastImportBlock.range, + } + } + + if (hasError) { + // syntax error at first import block, skip + return { + blockType: ImportClauseType.None, + } + } + + const importCtx: SuggestionContext['imports'] = { + blockType: ImportClauseType.None, + range: fallbackRange, + prependNewLine: true, + } + + // No imports, figure out if there is an empty line after package clause. + const nextLine = tokens[packagePos + 1] + if (nextLine && nextLine.findIndex(isNotEmptyToken) === -1) { + // line starts at 1. + const lineNo = packagePos + 2 + const colNo = model.getLineLength(lineNo) + 1 + importCtx.prependNewLine = false + importCtx.range = { + startLineNumber: lineNo, + endLineNumber: lineNo, + startColumn: colNo, + endColumn: colNo, + } + } + + return importCtx +} diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts index 32d9c543..df3b00a4 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts @@ -1,26 +1,23 @@ import type * as monaco from 'monaco-editor' -import type { SuggestionQuery } from '~/services/completion' +import type { SuggestionContext, SuggestionQuery } from '~/services/completion' import { asyncDebounce } from '../../utils' import snippets from './snippets' import { parseExpression } from './parse' import { CacheBasedCompletionProvider } from '../base' +import { buildImportContext } from './imports' const SUGGESTIONS_DEBOUNCE_DELAY = 500 -interface CompletionContext extends SuggestionQuery { - range: monaco.IRange -} - /** * Provides completion for symbols such as variables and functions. */ -export class GoSymbolsCompletionItemProvider extends CacheBasedCompletionProvider { +export class GoSymbolsCompletionItemProvider extends CacheBasedCompletionProvider { private readonly getSuggestionFunc = asyncDebounce( async (query) => await this.cache.getSymbolSuggestions(query), SUGGESTIONS_DEBOUNCE_DELAY, ) - protected getFallbackSuggestions({ value, range }: CompletionContext) { + protected getFallbackSuggestions({ value, context: { range } }: SuggestionQuery) { // filter snippets by prefix. // usually monaco does that but not always in right way const suggestions = snippets @@ -59,16 +56,24 @@ export class GoSymbolsCompletionItemProvider extends CacheBasedCompletionProvide endColumn: word.endColumn, } - return { ...query, range } + const context: SuggestionContext = { + range, + imports: buildImportContext(model), + } + + return { ...query, context } } - protected async querySuggestions(query: CompletionContext) { + protected async querySuggestions(query: SuggestionQuery) { const { suggestions: relatedSnippets } = this.getFallbackSuggestions(query) const suggestions = await this.getSuggestionFunc(query) if (!suggestions?.length) { return relatedSnippets } - return relatedSnippets.concat(suggestions.map((s) => ({ ...s, range: query.range }))) + const { + context: { range }, + } = query + return relatedSnippets.concat(suggestions.map((s) => ({ ...s, range }))) } } diff --git a/web/src/services/completion/types.ts b/web/src/services/completion/types.ts index 88e03546..a5d92e68 100644 --- a/web/src/services/completion/types.ts +++ b/web/src/services/completion/types.ts @@ -55,7 +55,7 @@ export interface SuggestionContext { * * @see prependNewLine */ - range: monaco.IRange + range?: monaco.IRange /** * Indicates whether extra new line should be appended before `import` clause. diff --git a/web/src/services/completion/utils.ts b/web/src/services/completion/utils.ts index 42dfbed9..7ce15859 100644 --- a/web/src/services/completion/utils.ts +++ b/web/src/services/completion/utils.ts @@ -55,7 +55,7 @@ const importPackageTextEdit = ( importPath: string, { imports }: SuggestionContext, ): ISingleEditOperation[] | undefined => { - if (imports.allPaths?.has(importPath)) { + if (!imports.range || imports.allPaths?.has(importPath)) { return undefined } From 7d14968cee70b0a6d7879892e69bad2ef2059828 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sun, 29 Sep 2024 12:36:10 -0400 Subject: [PATCH 04/43] feat: provide monitoring range --- .../workspace/CodeEditor/autocomplete/symbols/imports.ts | 8 ++++++++ web/src/services/completion/types.ts | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts index 15e334af..dda4047d 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts @@ -382,6 +382,10 @@ export const buildImportContext = (model: monaco.editor.ITextModel): SuggestionC blockPaths: lastImportBlock.imports.map(({ path }) => path), blockType: lastImportBlock.isMultiline ? ImportClauseType.Block : ImportClauseType.Single, range: lastImportBlock.range, + totalRange: { + startLineNumber: packageLine, + endLineNumber: lastImportBlock.range.endLineNumber, + }, } } @@ -396,6 +400,10 @@ export const buildImportContext = (model: monaco.editor.ITextModel): SuggestionC blockType: ImportClauseType.None, range: fallbackRange, prependNewLine: true, + totalRange: { + startLineNumber: packageLine, + endLineNumber: packageLine + 1, + }, } // No imports, figure out if there is an empty line after package clause. diff --git a/web/src/services/completion/types.ts b/web/src/services/completion/types.ts index a5d92e68..499f3f31 100644 --- a/web/src/services/completion/types.ts +++ b/web/src/services/completion/types.ts @@ -32,6 +32,13 @@ export interface SuggestionContext { */ allPaths?: Set + /** + * Start and end line of area containing all imports. + * + * This area will be monitored for changes to update document imports cache. + */ + totalRange?: Pick + /** * Imports in a last block related to `range`. */ From f9f576009a133f36c667dfd78b90f7e19819abcb Mon Sep 17 00:00:00 2001 From: x1unix Date: Sun, 29 Sep 2024 14:31:16 -0400 Subject: [PATCH 05/43] feat: use cache --- .../workspace/CodeEditor/CodeEditor.tsx | 18 +++- .../workspace/CodeEditor/autocomplete/base.ts | 6 +- .../CodeEditor/autocomplete/cache.ts | 69 ++++++++++++++ .../autocomplete/imports/provider.ts | 2 +- .../CodeEditor/autocomplete/index.ts | 1 + .../{symbols/imports.ts => parse.ts} | 8 +- .../CodeEditor/autocomplete/register.ts | 5 +- .../autocomplete/symbols/provider.ts | 23 +++-- .../autocomplete/symbols/snippets.ts | 7 +- web/src/services/completion/types.ts | 92 ++++++++++--------- 10 files changed, 161 insertions(+), 70 deletions(-) create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts rename web/src/components/features/workspace/CodeEditor/autocomplete/{symbols/imports.ts => parse.ts} (97%) diff --git a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx index 4c0f07e0..94081e4a 100644 --- a/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx +++ b/web/src/components/features/workspace/CodeEditor/CodeEditor.tsx @@ -18,7 +18,7 @@ import { getTimeNowUsageMarkers, asyncDebounce, debounce } from './utils' import { attachCustomCommands } from './commands' import { LANGUAGE_GOLANG, stateToOptions } from './props' import { configureMonacoLoader } from './loader' -import { registerGoLanguageProviders } from './autocomplete' +import { DocumentMetadataCache, registerGoLanguageProviders } from './autocomplete' import type { VimState } from '~/store/vim/state' const ANALYZE_DEBOUNCE_TIME = 500 @@ -66,6 +66,7 @@ class CodeEditor extends React.Component { private vimCommandAdapter?: StatusBarAdapter private monaco?: Monaco private disposables?: monaco.IDisposable[] + private readonly metadataCache = new DocumentMetadataCache() private readonly debouncedAnalyzeFunc = asyncDebounce(async (fileName: string, code: string) => { return await this.doAnalyze(fileName, code) @@ -80,7 +81,7 @@ class CodeEditor extends React.Component { }, 1000) editorDidMount(editorInstance: monaco.editor.IStandaloneCodeEditor, monacoInstance: Monaco) { - this.disposables = registerGoLanguageProviders(this.props.dispatch) + this.disposables = registerGoLanguageProviders(this.props.dispatch, this.metadataCache) this.editorInstance = editorInstance this.monaco = monacoInstance @@ -178,7 +179,11 @@ class CodeEditor extends React.Component { this.vimAdapter?.dispose() } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { + if (prevProps.projectId !== this.props.projectId) { + this.metadataCache.flush() + } + if (this.isFileOrEnvironmentChanged(prevProps)) { // Update editor markers on file or environment changes void this.debouncedAnalyzeFunc(this.props.fileName, this.props.code) @@ -191,6 +196,7 @@ class CodeEditor extends React.Component { this.disposables?.forEach((d) => d.dispose()) this.analyzer?.dispose() this.vimAdapter?.dispose() + this.metadataCache.flush() if (!this.editorInstance) { return @@ -202,13 +208,15 @@ class CodeEditor extends React.Component { this.editorInstance.dispose() } - onChange(newValue: string | undefined, _: monaco.editor.IModelContentChangedEvent) { + onChange(newValue: string | undefined, e: monaco.editor.IModelContentChangedEvent) { if (!newValue) { + this.metadataCache.flush(this.props.fileName) return } - this.props.dispatch(dispatchUpdateFile(this.props.fileName, newValue)) const { fileName, code } = this.props + this.metadataCache.handleUpdate(fileName, e) + this.props.dispatch(dispatchUpdateFile(fileName, newValue)) void this.debouncedAnalyzeFunc(fileName, code) } diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/base.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/base.ts index 7dce453d..0e420b88 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/base.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/base.ts @@ -14,11 +14,11 @@ const emptySuggestions = { suggestions: [] } export abstract class CacheBasedCompletionProvider implements monaco.languages.CompletionItemProvider { /** * @param dispatch Redux state dispatcher. Used to push notifications. - * @param cache Go completion cache service. + * @param completionSvc Go completion cache service. */ constructor( protected readonly dispatch: StateDispatch, - protected cache: GoCompletionService, + protected completionSvc: GoCompletionService, ) {} /** @@ -58,7 +58,7 @@ export abstract class CacheBasedCompletionProvider implements monaco.lan return emptySuggestions } - const shouldDisplayPreload = !this.cache.isWarmUp() + const shouldDisplayPreload = !this.completionSvc.isWarmUp() try { if (shouldDisplayPreload) { this.showLoadingProgress() diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts new file mode 100644 index 00000000..a5f2bce1 --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts @@ -0,0 +1,69 @@ +import type * as monaco from 'monaco-editor' +import type { ImportsContext } from '~/services/completion' +import { buildImportContext } from './parse' + +/** + * Stores document metadata (such as symbols, imports) in cache. + */ +export class DocumentMetadataCache { + private readonly cache = new Map() + + /** + * Flush cache contents. + * + * If fileName is not empty, flushes only record for specified file name. + */ + flush(fileName?: string) { + if (fileName) { + this.cache.delete(fileName) + return + } + + this.cache.clear() + } + + /** + * Invalidates cache if document is changed in imports range. + */ + handleUpdate(fileName: string, event: monaco.editor.IModelContentChangedEvent) { + const entry = this.cache.get(fileName) + if (!entry) { + return + } + + if (event.isFlush || !entry.totalRange) { + this.cache.delete(fileName) + return + } + + const { totalRange } = entry + for (const change of event.changes) { + const { startLineNumber } = change.range + + if (startLineNumber >= totalRange.startLineNumber && startLineNumber <= totalRange.endLineNumber) { + this.cache.delete(fileName) + return + } + } + } + + /** + * Returns document imports metadata from context. + * + * Populates data from model if it's not cached. + */ + getMetadata(fileName: string, model: monaco.editor.ITextModel) { + const data = this.cache.get(fileName) + if (data) { + return data + } + + return this.updateCache(fileName, model) + } + + private updateCache(fileName: string, model: monaco.editor.ITextModel) { + const context = buildImportContext(model) + this.cache.set(fileName, context) + return context + } +} diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/imports/provider.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/imports/provider.ts index e565c427..cd96a63a 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/imports/provider.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/imports/provider.ts @@ -14,7 +14,7 @@ export class GoImportsCompletionProvider extends CacheBasedCompletionProvider { - const suggestions = await this.cache.getImportSuggestions() + const suggestions = await this.completionSvc.getImportSuggestions() return suggestions } diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/index.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/index.ts index 3d4273ef..4c905644 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/index.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/index.ts @@ -1 +1,2 @@ export * from './register' +export * from './cache' diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts similarity index 97% rename from web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts rename to web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts index dda4047d..42ac03c9 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/imports.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts @@ -1,6 +1,6 @@ import * as monaco from 'monaco-editor' -import { ImportClauseType, type SuggestionContext } from '~/services/completion' +import { ImportClauseType, type ImportsContext } from '~/services/completion' type Tokens = monaco.Token[][] @@ -298,7 +298,7 @@ const traverseImportGroup = ( } } - throw new ParseError(header.line + 1, 1, 'unterminated import block') + throw new ParseError(header.line, 1, 'unterminated import block') } const findImportBlock = (offset: number, model: monaco.editor.ITextModel, tokens: Tokens): ImportBlock | null => { @@ -333,7 +333,7 @@ const findImportBlock = (offset: number, model: monaco.editor.ITextModel, tokens /** * Gathers information about Go imports in a model and provides information necessary for auto-import for suggestions. */ -export const buildImportContext = (model: monaco.editor.ITextModel): SuggestionContext['imports'] => { +export const buildImportContext = (model: monaco.editor.ITextModel): ImportsContext => { const tokens = monaco.editor.tokenize(model.getValue(), model.getLanguageId()) const packagePos = findPackageBlock(tokens) @@ -396,7 +396,7 @@ export const buildImportContext = (model: monaco.editor.ITextModel): SuggestionC } } - const importCtx: SuggestionContext['imports'] = { + const importCtx: ImportsContext = { blockType: ImportClauseType.None, range: fallbackRange, prependNewLine: true, diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/register.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/register.ts index 352668ec..3deb4a4c 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/register.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/register.ts @@ -4,15 +4,16 @@ import { GoSymbolsCompletionItemProvider } from './symbols' import { GoImportsCompletionProvider } from './imports' import type { StateDispatch } from '~/store' import { goCompletionService } from '~/services/completion' +import type { DocumentMetadataCache } from './cache' /** * Registers all Go autocomplete providers for Monaco editor. */ -export const registerGoLanguageProviders = (dispatcher: StateDispatch) => { +export const registerGoLanguageProviders = (dispatcher: StateDispatch, cache: DocumentMetadataCache) => { return [ monaco.languages.registerCompletionItemProvider( 'go', - new GoSymbolsCompletionItemProvider(dispatcher, goCompletionService), + new GoSymbolsCompletionItemProvider(dispatcher, goCompletionService, cache), ), monaco.languages.registerCompletionItemProvider( 'go', diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts index df3b00a4..a258be26 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts @@ -1,10 +1,11 @@ import type * as monaco from 'monaco-editor' -import type { SuggestionContext, SuggestionQuery } from '~/services/completion' +import type { StateDispatch } from '~/store' +import type { GoCompletionService, SuggestionContext, SuggestionQuery } from '~/services/completion' import { asyncDebounce } from '../../utils' import snippets from './snippets' import { parseExpression } from './parse' import { CacheBasedCompletionProvider } from '../base' -import { buildImportContext } from './imports' +import type { DocumentMetadataCache } from '../cache' const SUGGESTIONS_DEBOUNCE_DELAY = 500 @@ -12,18 +13,21 @@ const SUGGESTIONS_DEBOUNCE_DELAY = 500 * Provides completion for symbols such as variables and functions. */ export class GoSymbolsCompletionItemProvider extends CacheBasedCompletionProvider { + private readonly metadataCache: DocumentMetadataCache private readonly getSuggestionFunc = asyncDebounce( - async (query) => await this.cache.getSymbolSuggestions(query), + async (query) => await this.completionSvc.getSymbolSuggestions(query), SUGGESTIONS_DEBOUNCE_DELAY, ) - protected getFallbackSuggestions({ value, context: { range } }: SuggestionQuery) { + constructor(dispatch: StateDispatch, compSvc: GoCompletionService, metadataCache: DocumentMetadataCache) { + super(dispatch, compSvc) + this.metadataCache = metadataCache + } + + protected getFallbackSuggestions({ value, context: { range } }: SuggestionQuery): monaco.languages.CompletionList { // filter snippets by prefix. // usually monaco does that but not always in right way - const suggestions = snippets - // eslint-disable-next-line @typescript-eslint/no-base-to-string - .filter((s) => s.label.toString().startsWith(value)) - .map((s) => ({ ...s, range })) + const suggestions = snippets.filter((s) => s.label.startsWith(value)).map((s) => ({ ...s, range })) return { suggestions } } @@ -56,9 +60,10 @@ export class GoSymbolsCompletionItemProvider extends CacheBasedCompletionProvide endColumn: word.endColumn, } + const imports = this.metadataCache.getMetadata(model.uri.path, model) const context: SuggestionContext = { range, - imports: buildImportContext(model), + imports, } return { ...query, context } diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/snippets.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/snippets.ts index 22724784..a52ac8c1 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/snippets.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/snippets.ts @@ -3,6 +3,11 @@ import * as monaco from 'monaco-editor' /* eslint-disable no-template-curly-in-string */ const Rule = monaco.languages.CompletionItemInsertTextRule + +interface Snippet extends Omit { + label: string +} + /** * List of snippets for editor */ @@ -95,4 +100,4 @@ const snippets = [ ...s, })) -export default snippets as monaco.languages.CompletionItem[] +export default snippets as Snippet[] diff --git a/web/src/services/completion/types.ts b/web/src/services/completion/types.ts index 499f3f31..c1fca1c8 100644 --- a/web/src/services/completion/types.ts +++ b/web/src/services/completion/types.ts @@ -17,6 +17,52 @@ export enum ImportClauseType { Block, } +export interface ImportsContext { + /** + * List of import paths from all import blocks. + */ + allPaths?: Set + + /** + * Start and end line of area containing all imports. + * + * This area will be monitored for changes to update document imports cache. + */ + totalRange?: Pick + + /** + * Imports in a last block related to `range`. + */ + blockPaths?: string[] + + /** + * Type of nearest import block. + */ + blockType: ImportClauseType + + /** + * Position of nearest import block to insert new imports. + * + * If `blockType` is `ImportClauseType.None` - points to position + * of nearest empty line after `package` clause. + * + * If there is no empty line after `package` clause - should point + * to the end of clause statement + 1 extra column. + * + * Otherwise - should point to a full range of last `import` block. + * + * @see prependNewLine + */ + range?: monaco.IRange + + /** + * Indicates whether extra new line should be appended before `import` clause. + * + * Effective only when `range` is `ImportClauseType.None`. + */ + prependNewLine?: boolean +} + export interface SuggestionContext { /** * Current edit range @@ -26,51 +72,7 @@ export interface SuggestionContext { /** * Controls how auto import suggestions will be added. */ - imports: { - /** - * List of import paths from all import blocks. - */ - allPaths?: Set - - /** - * Start and end line of area containing all imports. - * - * This area will be monitored for changes to update document imports cache. - */ - totalRange?: Pick - - /** - * Imports in a last block related to `range`. - */ - blockPaths?: string[] - - /** - * Type of nearest import block. - */ - blockType: ImportClauseType - - /** - * Position of nearest import block to insert new imports. - * - * If `blockType` is `ImportClauseType.None` - points to position - * of nearest empty line after `package` clause. - * - * If there is no empty line after `package` clause - should point - * to the end of clause statement + 1 extra column. - * - * Otherwise - should point to a full range of last `import` block. - * - * @see prependNewLine - */ - range?: monaco.IRange - - /** - * Indicates whether extra new line should be appended before `import` clause. - * - * Effective only when `range` is `ImportClauseType.None`. - */ - prependNewLine?: boolean - } + imports: ImportsContext } export interface SuggestionQuery { From 0dc743a2396a21102ef6608f980eee7c077555be Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 16:47:54 -0400 Subject: [PATCH 06/43] feat: implement GOROOT indexer --- build.mk | 15 +- internal/analyzer/docfmt.go | 27 ++- internal/pkgindex/docutil/comments.go | 141 ++++++++++++ internal/pkgindex/docutil/decl.go | 57 +++++ internal/pkgindex/docutil/doc.go | 2 + internal/pkgindex/docutil/func.go | 53 +++++ internal/pkgindex/docutil/traverse.go | 62 ++++++ internal/pkgindex/docutil/type.go | 84 ++++++++ internal/pkgindex/docutil/utils.go | 147 +++++++++++++ internal/pkgindex/docutil/value.go | 59 +++++ internal/pkgindex/{ => imports}/parser.go | 30 +-- .../pkgindex/{ => imports}/parser_test.go | 2 +- internal/pkgindex/imports/queue.go | 37 ++++ internal/pkgindex/{ => imports}/result.go | 2 +- internal/pkgindex/{ => imports}/scanner.go | 21 +- .../pkgindex/{ => imports}/scanner_test.go | 2 +- .../testdata/badpkg/notagofile.txt | 0 .../testdata/foopkg/emptypkg/foo.go | 0 .../testdata/foopkg/pkgbar/doc.go | 0 .../testdata/foopkg/pkgbar/pkg.go | 0 internal/pkgindex/{ => imports}/utils.go | 17 +- internal/pkgindex/index/parse.go | 61 ++++++ internal/pkgindex/index/parse_test.go | 7 + internal/pkgindex/index/scanner.go | 202 ++++++++++++++++++ internal/pkgindex/index/types.go | 83 +++++++ internal/pkgindex/queue.go | 31 --- tools/pkgindexer/main.go | 107 +++++++--- web/src/services/completion/types.ts | 6 + 28 files changed, 1158 insertions(+), 97 deletions(-) create mode 100644 internal/pkgindex/docutil/comments.go create mode 100644 internal/pkgindex/docutil/decl.go create mode 100644 internal/pkgindex/docutil/doc.go create mode 100644 internal/pkgindex/docutil/func.go create mode 100644 internal/pkgindex/docutil/traverse.go create mode 100644 internal/pkgindex/docutil/type.go create mode 100644 internal/pkgindex/docutil/utils.go create mode 100644 internal/pkgindex/docutil/value.go rename internal/pkgindex/{ => imports}/parser.go (67%) rename internal/pkgindex/{ => imports}/parser_test.go (99%) create mode 100644 internal/pkgindex/imports/queue.go rename internal/pkgindex/{ => imports}/result.go (91%) rename internal/pkgindex/{ => imports}/scanner.go (91%) rename internal/pkgindex/{ => imports}/scanner_test.go (97%) rename internal/pkgindex/{ => imports}/testdata/badpkg/notagofile.txt (100%) rename internal/pkgindex/{ => imports}/testdata/foopkg/emptypkg/foo.go (100%) rename internal/pkgindex/{ => imports}/testdata/foopkg/pkgbar/doc.go (100%) rename internal/pkgindex/{ => imports}/testdata/foopkg/pkgbar/pkg.go (100%) rename internal/pkgindex/{ => imports}/utils.go (74%) create mode 100644 internal/pkgindex/index/parse.go create mode 100644 internal/pkgindex/index/parse_test.go create mode 100644 internal/pkgindex/index/scanner.go create mode 100644 internal/pkgindex/index/types.go delete mode 100644 internal/pkgindex/queue.go diff --git a/build.mk b/build.mk index 4b223d7a..efce0492 100644 --- a/build.mk +++ b/build.mk @@ -32,10 +32,15 @@ check-go: exit 1; \ fi -.PHONY: pkg-index -pkg-index: - @echo ":: Generating Go packages index..." && \ - $(GO) run ./tools/pkgindexer -o $(UI)/public/data/imports.json +.PHONY: imports-index +imports-index: + @echo ":: Generating Go imports index..." && \ + $(GO) run ./tools/pkgindexer imports -o $(UI)/public/data/imports.json + +.PHONY: go-index +go-index: + @echo ":: Generating Go symbols index..." && \ + $(GO) run ./tools/pkgindexer index -o $(UI)/public/data/go-index.json .PHONY:check-yarn check-yarn: @@ -68,7 +73,7 @@ analyzer.wasm: wasm: wasm_exec.js analyzer.wasm .PHONY: build -build: check-go check-yarn clean preinstall gen build-server wasm pkg-index build-ui +build: check-go check-yarn clean preinstall gen build-server wasm go-index imports-index build-ui @echo ":: Copying assets..." && \ cp -rfv ./data $(TARGET)/data && \ mv -v $(UI)/build $(TARGET)/public && \ diff --git a/internal/analyzer/docfmt.go b/internal/analyzer/docfmt.go index bf126885..b61aefb9 100644 --- a/internal/analyzer/docfmt.go +++ b/internal/analyzer/docfmt.go @@ -1,6 +1,11 @@ package analyzer -import "strings" +import ( + "bytes" + "go/ast" + "go/doc/comment" + "strings" +) const ( newLineChar = '\n' @@ -27,7 +32,27 @@ func isDocLine(line string) bool { return false } +// ParseCommentGroup parses comments from AST and returns them in Markdown format. +func ParseCommentGroup(group *ast.CommentGroup) []byte { + if group == nil || len(group.List) == 0 { + return nil + } + + var ( + parser comment.Parser + printer comment.Printer + ) + + str := group.Text() + parsedDoc := parser.Parse(str) + mdDoc := printer.Markdown(parsedDoc) + mdDoc = bytes.TrimSuffix(mdDoc, []byte("\n")) + return mdDoc +} + // FormatDocString parses Go comment and returns a markdown-formatted string. +// +// Deprecated: use ParseCommentGroup instead. func FormatDocString(str string) MarkdownString { if str == "" { return MarkdownString{Value: str} diff --git a/internal/pkgindex/docutil/comments.go b/internal/pkgindex/docutil/comments.go new file mode 100644 index 00000000..fdca45c3 --- /dev/null +++ b/internal/pkgindex/docutil/comments.go @@ -0,0 +1,141 @@ +package docutil + +import ( + "bytes" + "go/ast" + "go/doc/comment" + "strings" + "unicode" +) + +const ( + pkgDocPrefix = "package " + goDocBaseUrl = "https://pkg.go.dev/" + + // Char count of static markup chars from writeGoDocLink. + goDocCharsLen = 20 +) + +// BuildPackageDoc builds markdown documentation for package with a link to GoDoc page. +// +// Please call [IsPackageDoc] before using this method. +func BuildPackageDoc(group *ast.CommentGroup, importPath string) string { + src := FormatCommentGroup(group) + src = bytes.TrimSpace(src) + + return appendGoDocLink(src, importPath) +} + +// EmptyPackageDoc returns empty package doc which only contains link to GoDoc. +func EmptyPackageDoc(importPath string) string { + return appendGoDocLink(nil, importPath) +} + +func appendGoDocLink(docStr []byte, importPath string) string { + // Approx new buffer length. 20 is length of static characters. + newLen := len(docStr) + len(importPath) + len(importPath) + len(goDocBaseUrl) + goDocCharsLen + sb := new(strings.Builder) + sb.Grow(newLen) + sb.Write(docStr) + + // [pkgName on pkg.go.dev](https://pkg.go.dev/importPath) + if len(docStr) > 0 { + sb.WriteString("\n\n[") + } else { + sb.WriteString("[") + } + + sb.WriteString(importPath) + sb.WriteString(" on pkg.go.dev](") + sb.WriteString(goDocBaseUrl) + sb.WriteString(importPath) + sb.WriteRune(')') + return sb.String() +} + +// FormatCommentGroup parses comments from AST and returns them in Markdown format. +func FormatCommentGroup(group *ast.CommentGroup) []byte { + if group == nil || len(group.List) == 0 { + return nil + } + + var ( + parser comment.Parser + printer comment.Printer + ) + + str := group.Text() + parsedDoc := parser.Parse(str) + mdDoc := printer.Markdown(parsedDoc) + mdDoc = bytes.TrimSuffix(mdDoc, []byte("\n")) + return mdDoc +} + +// IsPackageDoc returns whether top level comment is a valid package comment. +// +// See issue #367. +func IsPackageDoc(group *ast.CommentGroup) bool { + if group == nil || len(group.List) == 0 { + return false + } + + for _, comment := range group.List { + c := strings.TrimPrefix(comment.Text, "//") + c = strings.TrimPrefix(c, "/*") + c = strings.TrimLeftFunc(c, unicode.IsSpace) + if c == "" || isDirective(c) { + continue + } + + // We interested only in first non-empty comment. + return hasPackagePrefix(c) + } + + return false +} + +func hasPackagePrefix(c string) bool { + if len(c) < len(pkgDocPrefix) { + return false + } + + pfx := []rune(pkgDocPrefix) + for i, char := range c[len(pkgDocPrefix):] { + char = unicode.ToLower(char) + if char != pfx[i] { + return false + } + } + + return true +} + +// isDirective reports whether c is a comment directive. +// +// Copy from go/ast/ast.go:172 +func isDirective(c string) bool { + // "//line " is a line directive. + // "//extern " is for gccgo. + // "//export " is for cgo. + // (The // has been removed.) + if strings.HasPrefix(c, "line ") || strings.HasPrefix(c, "extern ") || strings.HasPrefix(c, "export ") { + return true + } + + // "//[a-z0-9]+:[a-z0-9]" + // (The // has been removed.) + colon := strings.Index(c, ":") + if colon <= 0 || colon+1 >= len(c) { + return false + } + for i := 0; i <= colon+1; i++ { + if i == colon { + continue + } + b := c[i] + if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') { + return false + } + } + return true +} diff --git a/internal/pkgindex/docutil/decl.go b/internal/pkgindex/docutil/decl.go new file mode 100644 index 00000000..73fb604b --- /dev/null +++ b/internal/pkgindex/docutil/decl.go @@ -0,0 +1,57 @@ +package docutil + +import ( + "fmt" + "github.com/x1unix/go-playground/pkg/monaco" + "go/ast" + "go/token" +) + +func DeclToCompletionItem(fset *token.FileSet, specGroup *ast.GenDecl, allowUnexported bool) ([]monaco.CompletionItem, error) { + if len(specGroup.Specs) == 0 { + return nil, nil + } + + block, err := NewBlockData(specGroup) + if err != nil { + return nil, err + } + + // block declarations have documentation inside block child, e.g: + // var ( + // // doc + // foo = 1 + // ) + + completions := make([]monaco.CompletionItem, 0, len(specGroup.Specs)) + for _, spec := range specGroup.Specs { + switch t := spec.(type) { + case *ast.TypeSpec: + if !t.Name.IsExported() && !allowUnexported { + continue + } + + item, err := TypeToCompletionItem(fset, block, t) + if err != nil { + return nil, err + } + + completions = append(completions, item) + case *ast.ValueSpec: + items, err := ValueToCompletionItem(fset, block, t, allowUnexported) + if err != nil { + return nil, err + } + + if len(items) == 0 { + continue + } + + completions = append(completions, items...) + default: + return nil, fmt.Errorf("unsupported declaration type %T", t) + } + } + + return completions, nil +} diff --git a/internal/pkgindex/docutil/doc.go b/internal/pkgindex/docutil/doc.go new file mode 100644 index 00000000..e7ed7752 --- /dev/null +++ b/internal/pkgindex/docutil/doc.go @@ -0,0 +1,2 @@ +// Package docutil provides common primitives for documentation generation utilities. +package docutil diff --git a/internal/pkgindex/docutil/func.go b/internal/pkgindex/docutil/func.go new file mode 100644 index 00000000..f2f94e4d --- /dev/null +++ b/internal/pkgindex/docutil/func.go @@ -0,0 +1,53 @@ +package docutil + +import ( + "go/ast" + "go/token" + "strings" + + "github.com/x1unix/go-playground/pkg/monaco" +) + +// CompletionItemFromFunc constructs completion item from a function AST declaration. +// +// Function documentation is generated in Markdown format. +func CompletionItemFromFunc(fset *token.FileSet, fn *ast.FuncDecl, snippetFormat monaco.CompletionItemInsertTextRule) (item monaco.CompletionItem, err error) { + isSnippet := snippetFormat == monaco.InsertAsSnippet + item = monaco.CompletionItem{ + Kind: monaco.Function, + InsertTextRules: snippetFormat, + InsertText: buildFuncInsertStatement(fn, isSnippet), + } + + item.Label.SetString(fn.Name.String()) + item.Documentation.SetValue(&monaco.IMarkdownString{ + Value: string(FormatCommentGroup(fn.Doc)), + }) + + // TODO: ensure that body is removed + item.Detail, err = DeclToString(fset, fn) + if err != nil { + return item, err + } + + return item, nil +} + +func buildFuncInsertStatement(decl *ast.FuncDecl, asSnippet bool) string { + if !asSnippet { + return decl.Name.String() + "()" + } + + // snippet offsets start at 1 + offset := 1 + + typ := decl.Type + sb := new(strings.Builder) + sb.Grow(defaultStringBuffSize) + sb.WriteString(decl.Name.String()) + offset = WriteTypeParams(sb, offset, typ.TypeParams) + sb.WriteString("(") + WriteParamsList(sb, offset, typ.Params) + sb.WriteString(")") + return sb.String() +} diff --git a/internal/pkgindex/docutil/traverse.go b/internal/pkgindex/docutil/traverse.go new file mode 100644 index 00000000..134ec971 --- /dev/null +++ b/internal/pkgindex/docutil/traverse.go @@ -0,0 +1,62 @@ +package docutil + +import ( + "fmt" + "go/ast" + "go/token" + "log" + + "github.com/x1unix/go-playground/pkg/monaco" +) + +type TraverseOpts struct { + AllowUnexported bool + FileSet *token.FileSet + SnippetFormat monaco.CompletionItemInsertTextRule +} + +type TraverseReducer = func(items ...monaco.CompletionItem) + +// CollectCompletionItems traverses root file declarations and transforms them into completion items. +func CollectCompletionItems(decls []ast.Decl, opts TraverseOpts, reducer TraverseReducer) error { + for _, decl := range decls { + switch t := decl.(type) { + case *ast.FuncDecl: + if !t.Name.IsExported() && !opts.AllowUnexported { + continue + } + + item, err := CompletionItemFromFunc(opts.FileSet, t, monaco.InsertAsSnippet) + if err != nil { + return fmt.Errorf( + "can't parse function %s: %w (pos: %s)", + t.Name.String(), err, GetDeclPosition(opts.FileSet, t), + ) + } + + reducer(item) + case *ast.GenDecl: + if t.Tok == token.IMPORT { + continue + } + + items, err := DeclToCompletionItem(opts.FileSet, t, opts.AllowUnexported) + if err != nil { + return fmt.Errorf( + "can't parse decl %s: %w (at %s)", + t.Tok, err, GetDeclPosition(opts.FileSet, t), + ) + } + + reducer(items...) + default: + fname := opts.FileSet.File(decl.Pos()).Name() + log.Printf( + "Warning: unsupported block %T at %s:%s", + t, fname, GetDeclPosition(opts.FileSet, decl), + ) + } + } + + return nil +} diff --git a/internal/pkgindex/docutil/type.go b/internal/pkgindex/docutil/type.go new file mode 100644 index 00000000..6b8d28f5 --- /dev/null +++ b/internal/pkgindex/docutil/type.go @@ -0,0 +1,84 @@ +package docutil + +import ( + "fmt" + "go/ast" + "go/token" + + "github.com/x1unix/go-playground/pkg/monaco" +) + +// BlockData contains information about declaration group. +type BlockData struct { + IsGroup bool + Decl *ast.GenDecl + Kind monaco.CompletionItemKind +} + +// NewBlockData parses block data from AST block declaration node. +// +// Example: +// +// var ( +// foo = 1 +// bar = "bar" +// ) +// +// type ( +// foo int +// bar struct{} +// ) +func NewBlockData(specGroup *ast.GenDecl) (BlockData, error) { + blockKind, ok := TokenToCompletionItemKind(specGroup.Tok) + if !ok { + return BlockData{}, fmt.Errorf("unsupported declaration token %q", specGroup.Tok) + } + + return BlockData{ + Decl: specGroup, + Kind: blockKind, + IsGroup: len(specGroup.Specs) > 1, + }, nil +} + +// TypeToCompletionItem returns completion item from type declaration inside block. +func TypeToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.TypeSpec) (monaco.CompletionItem, error) { + // Block declarations contain doc inside each child. + declCommentGroup := spec.Comment + if block.IsGroup { + declCommentGroup = block.Decl.Doc + } + + item := monaco.CompletionItem{ + Kind: block.Kind, + InsertText: spec.Name.Name, + } + item.Label.String = spec.Name.Name + + isPrimitive := false + switch spec.Type.(type) { + case *ast.InterfaceType: + item.Kind = monaco.Interface + case *ast.StructType: + // TODO: prefill struct members + item.InsertText = item.InsertText + "{}" + item.Kind = monaco.Struct + case *ast.Ident: + isPrimitive = true + } + + if !isPrimitive { + signature, err := DeclToString(fset, block.Decl) + if err != nil { + return item, fmt.Errorf("%w (type: %q, pos: %s)", err, item.Label, GetDeclPosition(fset, spec)) + } + + item.Detail = signature + } + + item.Documentation.SetValue(&monaco.IMarkdownString{ + Value: string(FormatCommentGroup(declCommentGroup)), + }) + + return item, nil +} diff --git a/internal/pkgindex/docutil/utils.go b/internal/pkgindex/docutil/utils.go new file mode 100644 index 00000000..5649f1ca --- /dev/null +++ b/internal/pkgindex/docutil/utils.go @@ -0,0 +1,147 @@ +package docutil + +import ( + "fmt" + "go/ast" + "go/printer" + "go/token" + "reflect" + "strconv" + "strings" + + "github.com/x1unix/go-playground/pkg/monaco" +) + +const defaultStringBuffSize = 64 + +const BuiltinPackage = "builtin" + +var ( + astDocFields = []string{"Doc", "Comment"} + commentBlockType = reflect.TypeOf((*ast.CommentGroup)(nil)) + + token2KindMapping = map[token.Token]monaco.CompletionItemKind{ + token.VAR: monaco.Variable, + token.CONST: monaco.Constant, + token.TYPE: monaco.Class, + } +) + +// TokenToCompletionItemKind maps Go AST token to monaco's completion item kind. +func TokenToCompletionItemKind(tok token.Token) (monaco.CompletionItemKind, bool) { + k, ok := token2KindMapping[tok] + return k, ok +} + +// GetDeclRange returns AST node range in document. +func GetDeclRange(fset *token.FileSet, decl ast.Decl) (start token.Position, end token.Position) { + f := fset.File(decl.Pos()) + + start = f.Position(decl.Pos()) + end = f.Position(decl.End()) + return start, end +} + +type Positioner interface { + Pos() token.Pos +} + +// GetDeclPosition returns start position of an AST node +func GetDeclPosition(fset *token.FileSet, decl Positioner) token.Position { + return OffsetToPosition(fset, decl.Pos()) +} + +// OffsetToPosition translates offset into position with column and line number. +func OffsetToPosition(fset *token.FileSet, pos token.Pos) token.Position { + f := fset.File(pos) + return f.Position(pos) +} + +// WriteTypeParams writes type parameters snippet template into a specified buffer. +// +// Snippet offset is a start index for snippet template variables (`$n`) to fill parameters. +func WriteTypeParams(sb *strings.Builder, snippetIndex int, typeParams *ast.FieldList) int { + if typeParams == nil || len(typeParams.List) == 0 { + return snippetIndex + } + + sb.WriteRune('[') + offset := WriteParamsList(sb, snippetIndex, typeParams) + sb.WriteRune(']') + return offset +} + +// WriteParamsList writes parameters list template (usually func args) into a specified buffer. +// +// Snippet offset is a start index for snippet template variables (`$n`) to fill parameters. +func WriteParamsList(sb *strings.Builder, snippetIndex int, params *ast.FieldList) int { + if params == nil || len(params.List) == 0 { + return snippetIndex + } + + offset := snippetIndex + for i, arg := range params.List { + if i > 0 { + sb.WriteString(", ") + } + + for j, n := range arg.Names { + if j > 0 { + sb.WriteString(", ") + } + + sb.WriteString("${") + sb.WriteString(strconv.Itoa(offset)) + sb.WriteRune(':') + sb.WriteString(n.String()) + sb.WriteRune('}') + offset++ + } + } + + return offset +} + +// DeclToString returns string representation of passed AST node. +func DeclToString(fset *token.FileSet, decl any) (string, error) { + // Remove comments block from AST node to keep only node body + trimmedDecl := removeCommentFromDecl(decl) + + sb := new(strings.Builder) + sb.Grow(defaultStringBuffSize) + err := printer.Fprint(sb, fset, trimmedDecl) + if err != nil { + return "", fmt.Errorf("can't generate type signature out of AST node %T: %w", trimmedDecl, err) + } + + return sb.String(), nil +} + +func removeCommentFromDecl(decl any) any { + val := reflect.ValueOf(decl) + isPtr := val.Kind() == reflect.Pointer + if isPtr { + val = val.Elem() + } + if val.Kind() != reflect.Struct { + return decl + } + + dst := reflect.New(val.Type()).Elem() + dst.Set(val) + + // *ast.FuncDecl, *ast.Object have Doc + // *ast.Object and *ast.Indent might have Comment + for _, fieldName := range astDocFields { + field, ok := val.Type().FieldByName(fieldName) + if ok && field.Type.AssignableTo(commentBlockType) { + dst.FieldByIndex(field.Index).SetZero() + } + } + + if isPtr { + dst = dst.Addr() + } + + return dst.Interface() +} diff --git a/internal/pkgindex/docutil/value.go b/internal/pkgindex/docutil/value.go new file mode 100644 index 00000000..a6fd0cbd --- /dev/null +++ b/internal/pkgindex/docutil/value.go @@ -0,0 +1,59 @@ +package docutil + +import ( + "fmt" + "go/ast" + "go/token" + + "github.com/x1unix/go-playground/pkg/monaco" +) + +// ValueToCompletionItem constructs completion item from value declaration. +// +// Able to handle special edge cases for builtin declarations. +func ValueToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.ValueSpec, allowUnexported bool) ([]monaco.CompletionItem, error) { + var blockDoc *monaco.IMarkdownString + if !block.IsGroup { + blockDoc = &monaco.IMarkdownString{ + Value: string(FormatCommentGroup(spec.Doc)), + } + } + + items := make([]monaco.CompletionItem, 0, len(spec.Values)) + for _, val := range spec.Names { + if !val.IsExported() && !allowUnexported { + continue + } + + item := monaco.CompletionItem{ + Kind: block.Kind, + InsertText: val.Name, + } + + item.Label.String = val.Name + item.Documentation.SetValue(blockDoc) + + switch val.Name { + case "true", "false": + // TODO: handle builtins + default: + signature, err := DeclToString(fset, val.Obj.Decl) + if err != nil { + return nil, fmt.Errorf( + "%w (value name: %s, pos: %s)", err, val.Name, GetDeclPosition(fset, val), + ) + } + + // declaration type is not present in value block. + if signature != "" { + signature = block.Decl.Tok.String() + " " + signature + } + + item.Detail = signature + } + + items = append(items, item) + } + + return items, nil +} diff --git a/internal/pkgindex/parser.go b/internal/pkgindex/imports/parser.go similarity index 67% rename from internal/pkgindex/parser.go rename to internal/pkgindex/imports/parser.go index 3e22d8b5..42a8e575 100644 --- a/internal/pkgindex/parser.go +++ b/internal/pkgindex/imports/parser.go @@ -1,4 +1,4 @@ -package pkgindex +package imports import ( "context" @@ -6,14 +6,11 @@ import ( "go/parser" "go/token" "path/filepath" - "strings" - "github.com/x1unix/go-playground/internal/analyzer" + "github.com/x1unix/go-playground/internal/pkgindex/docutil" "github.com/x1unix/go-playground/pkg/monaco" ) -const goDocDomain = "pkg.go.dev" - type PackageParseParams struct { RootDir string ImportPath string @@ -24,10 +21,6 @@ func (params PackageParseParams) PackagePath() string { return filepath.Join(params.RootDir, params.ImportPath) } -func formatGoDocLink(importPath string) string { - return fmt.Sprintf("[%[2]s on %[1]s](https://%[1]s/%[2]s)", goDocDomain, importPath) -} - // ParseImportCompletionItem parses a Go package at a given GOROOT and constructs monaco CompletionItem from it. func ParseImportCompletionItem(ctx context.Context, params PackageParseParams) (result monaco.CompletionItem, error error) { pkgPath := params.PackagePath() @@ -38,7 +31,7 @@ func ParseImportCompletionItem(ctx context.Context, params PackageParseParams) ( InsertText: params.ImportPath, } - docString := formatGoDocLink(params.ImportPath) + var docString string fset := token.NewFileSet() for _, fname := range params.Files { @@ -60,14 +53,14 @@ func ParseImportCompletionItem(ctx context.Context, params PackageParseParams) ( result.Detail = src.Name.String() // Package doc should start with "Package xxx", see issue #367. - docComment := strings.TrimSpace(doc.Text()) - if !validatePackageDoc(docComment) { - continue + if docutil.IsPackageDoc(doc) { + docString = docutil.BuildPackageDoc(doc, params.ImportPath) + break } + } - docComment = strings.TrimSpace(analyzer.FormatDocString(docComment).Value) - docString = docComment + "\n\n" + docString - break + if docString == "" { + docString = docutil.EmptyPackageDoc(params.ImportPath) } result.Label.SetString(params.ImportPath) @@ -77,8 +70,3 @@ func ParseImportCompletionItem(ctx context.Context, params PackageParseParams) ( }) return result, nil } - -func validatePackageDoc(str string) bool { - normalizedStr := strings.ToLower(str) - return strings.HasPrefix(normalizedStr, "package ") -} diff --git a/internal/pkgindex/parser_test.go b/internal/pkgindex/imports/parser_test.go similarity index 99% rename from internal/pkgindex/parser_test.go rename to internal/pkgindex/imports/parser_test.go index 650da295..db98f6d0 100644 --- a/internal/pkgindex/parser_test.go +++ b/internal/pkgindex/imports/parser_test.go @@ -1,4 +1,4 @@ -package pkgindex +package imports import ( "context" diff --git a/internal/pkgindex/imports/queue.go b/internal/pkgindex/imports/queue.go new file mode 100644 index 00000000..1311463d --- /dev/null +++ b/internal/pkgindex/imports/queue.go @@ -0,0 +1,37 @@ +package imports + +type Queue[T any] struct { + entries []T + max int +} + +func NewQueue[T any](size int) *Queue[T] { + return &Queue[T]{entries: make([]T, 0, size)} +} + +func (q *Queue[T]) Occupied() bool { + return len(q.entries) != 0 +} + +func (q *Queue[T]) MaxOccupancy() int { + return q.max +} + +func (q *Queue[T]) Pop() (val T, ok bool) { + if len(q.entries) == 0 { + return val, false + } + + entry := q.entries[0] + q.entries = q.entries[1:] + return entry, true +} + +func (q *Queue[T]) Add(items ...T) { + if len(items) == 0 { + return + } + + q.entries = append(q.entries, items...) + q.max = max(len(q.entries), q.max) +} diff --git a/internal/pkgindex/result.go b/internal/pkgindex/imports/result.go similarity index 91% rename from internal/pkgindex/result.go rename to internal/pkgindex/imports/result.go index 2e8d9f41..787f2906 100644 --- a/internal/pkgindex/result.go +++ b/internal/pkgindex/imports/result.go @@ -1,4 +1,4 @@ -package pkgindex +package imports import "github.com/x1unix/go-playground/pkg/monaco" diff --git a/internal/pkgindex/scanner.go b/internal/pkgindex/imports/scanner.go similarity index 91% rename from internal/pkgindex/scanner.go rename to internal/pkgindex/imports/scanner.go index 01999502..aeb7b7ba 100644 --- a/internal/pkgindex/scanner.go +++ b/internal/pkgindex/imports/scanner.go @@ -1,6 +1,6 @@ -// Package pkgindex implements functionality for generating Monaco code completion data +// Package imports implements functionality for generating Monaco code completion data // from documentation and symbols extracted from Go source files. -package pkgindex +package imports import ( "bufio" @@ -43,7 +43,7 @@ func NewGoRootScanner(goRoot string) GoRootScanner { } func (s *GoRootScanner) Scan() (*GoRootSummary, error) { - version, err := checkVersion(s.goRoot) + version, err := CheckVersionFile(s.goRoot) if err != nil { return nil, fmt.Errorf("can't extract Go SDK version: %w", err) } @@ -66,24 +66,23 @@ func (s *GoRootScanner) start() ([]monaco.CompletionItem, error) { return nil, fmt.Errorf("cannot open Go SDK directory: %w", err) } - q := newQueue(queueInitSize) + q := NewQueue[string](queueInitSize) for _, entry := range entries { if !entry.Type().IsDir() { continue } pkgName := entry.Name() - switch pkgName { - case "cmd", "internal", "vendor", "builtin": + if IsDirIgnored(pkgName, true) { continue } - q.add(pkgName) + q.Add(pkgName) } results := make([]monaco.CompletionItem, 0, resultsInitSize) - for q.occupied() { - pkgName, ok := q.pop() + for q.Occupied() { + pkgName, ok := q.Pop() if !ok { break } @@ -93,7 +92,7 @@ func (s *GoRootScanner) start() ([]monaco.CompletionItem, error) { return nil, err } - q.add(result.children...) + q.Add(result.children...) if result.hasPkg { results = append(results, result.item) } @@ -147,7 +146,7 @@ func (s *GoRootScanner) visitPackage(rootDir string, importPath string) (scanRes }, nil } -func checkVersion(root string) (string, error) { +func CheckVersionFile(root string) (string, error) { f, err := os.Open(filepath.Join(root, "VERSION")) if err != nil { return "", fmt.Errorf("cannot open version file: %w", err) diff --git a/internal/pkgindex/scanner_test.go b/internal/pkgindex/imports/scanner_test.go similarity index 97% rename from internal/pkgindex/scanner_test.go rename to internal/pkgindex/imports/scanner_test.go index 2ea83adc..d4877131 100644 --- a/internal/pkgindex/scanner_test.go +++ b/internal/pkgindex/imports/scanner_test.go @@ -1,4 +1,4 @@ -package pkgindex +package imports import ( "testing" diff --git a/internal/pkgindex/testdata/badpkg/notagofile.txt b/internal/pkgindex/imports/testdata/badpkg/notagofile.txt similarity index 100% rename from internal/pkgindex/testdata/badpkg/notagofile.txt rename to internal/pkgindex/imports/testdata/badpkg/notagofile.txt diff --git a/internal/pkgindex/testdata/foopkg/emptypkg/foo.go b/internal/pkgindex/imports/testdata/foopkg/emptypkg/foo.go similarity index 100% rename from internal/pkgindex/testdata/foopkg/emptypkg/foo.go rename to internal/pkgindex/imports/testdata/foopkg/emptypkg/foo.go diff --git a/internal/pkgindex/testdata/foopkg/pkgbar/doc.go b/internal/pkgindex/imports/testdata/foopkg/pkgbar/doc.go similarity index 100% rename from internal/pkgindex/testdata/foopkg/pkgbar/doc.go rename to internal/pkgindex/imports/testdata/foopkg/pkgbar/doc.go diff --git a/internal/pkgindex/testdata/foopkg/pkgbar/pkg.go b/internal/pkgindex/imports/testdata/foopkg/pkgbar/pkg.go similarity index 100% rename from internal/pkgindex/testdata/foopkg/pkgbar/pkg.go rename to internal/pkgindex/imports/testdata/foopkg/pkgbar/pkg.go diff --git a/internal/pkgindex/utils.go b/internal/pkgindex/imports/utils.go similarity index 74% rename from internal/pkgindex/utils.go rename to internal/pkgindex/imports/utils.go index 0bf37928..67b61f3d 100644 --- a/internal/pkgindex/utils.go +++ b/internal/pkgindex/imports/utils.go @@ -1,4 +1,4 @@ -package pkgindex +package imports import ( "bytes" @@ -42,3 +42,18 @@ func ResolveGoRoot() (string, error) { return goroot, nil } + +func IsDirIgnored(basename string, ignoreVendor bool) bool { + switch basename { + case "cmd", "internal", "builtin", "testdata": + return true + case "vendor": + return ignoreVendor + } + + return false +} + +func IsVendorDir(dirName string) bool { + return dirName == "vendor" +} diff --git a/internal/pkgindex/index/parse.go b/internal/pkgindex/index/parse.go new file mode 100644 index 00000000..c7460066 --- /dev/null +++ b/internal/pkgindex/index/parse.go @@ -0,0 +1,61 @@ +package index + +import ( + "github.com/x1unix/go-playground/internal/pkgindex/docutil" + "github.com/x1unix/go-playground/pkg/monaco" + "go/ast" + "go/parser" + "go/token" +) + +type sourceSummary struct { + packageName string + doc *ast.CommentGroup + symbols []SymbolInfo +} + +type fileParseParams struct { + importPath string + parseDoc bool +} + +func parseFile(fset *token.FileSet, fpath string, params fileParseParams) (*sourceSummary, error) { + root, err := parser.ParseFile(fset, fpath, nil, parser.ParseComments) + if err != nil { + return nil, err + } + + summary := sourceSummary{ + packageName: root.Name.String(), + symbols: make([]SymbolInfo, 0, len(root.Decls)), + } + + // "go/doc" ignores some packages from GOROOT thus it doesn't work for us. + // That means, all boring job should be done manually. + if params.parseDoc && docutil.IsPackageDoc(root.Doc) { + summary.doc = root.Doc + } + + src := SymbolSource{ + Name: root.Name.String(), + Path: params.importPath, + } + + opts := docutil.TraverseOpts{ + AllowUnexported: params.importPath == docutil.BuiltinPackage, + FileSet: fset, + SnippetFormat: monaco.InsertAsSnippet, + } + + err = docutil.CollectCompletionItems(root.Decls, opts, func(items ...monaco.CompletionItem) { + for _, item := range items { + summary.symbols = append(summary.symbols, SymbolInfoFromCompletionItem(item, src)) + } + }) + + if err != nil { + return nil, err + } + + return &summary, nil +} diff --git a/internal/pkgindex/index/parse_test.go b/internal/pkgindex/index/parse_test.go new file mode 100644 index 00000000..f46e458e --- /dev/null +++ b/internal/pkgindex/index/parse_test.go @@ -0,0 +1,7 @@ +package index + +import "testing" + +func TestScanRoot(t *testing.T) { + // TODO +} diff --git a/internal/pkgindex/index/scanner.go b/internal/pkgindex/index/scanner.go new file mode 100644 index 00000000..5be30b4c --- /dev/null +++ b/internal/pkgindex/index/scanner.go @@ -0,0 +1,202 @@ +package index + +import ( + "fmt" + "go/token" + "log" + "os" + "path" + "path/filepath" + "runtime" + "strings" + + "github.com/x1unix/go-playground/internal/pkgindex/docutil" + "github.com/x1unix/go-playground/internal/pkgindex/imports" +) + +// queueSize is based on max occupation of a queue during test scan of Go 1.23. +// +// See: Queue.MaxOccupancy +const queueSize = 120 + +type scanEntry struct { + isVendor bool + path string + importPath string +} + +func (ent scanEntry) makeChild(dirName string) scanEntry { + return scanEntry{ + path: filepath.Join(ent.path, dirName), + importPath: path.Join(ent.importPath, dirName), + } +} + +func ScanRoot(goRoot string) (*GoIndexFile, error) { + goVersion, err := imports.CheckVersionFile(goRoot) + if err != nil { + log.Printf("Warning: can't read version file, using fallback. Error: %s", err) + goVersion = strings.TrimPrefix(runtime.Version(), "go") + } + + // populate queue with root packages + rootDir := filepath.Join(goRoot, "src") + queue := imports.NewQueue[scanEntry](queueSize) + if err := enqueueRootEntries(rootDir, "", queue); err != nil { + return nil, err + } + + // There are 213 packages in Go 1.23 + packages := make([]PackageInfo, 0, 213) + symbols := make([]SymbolInfo, 0, 300) + + for queue.Occupied() { + v, ok := queue.Pop() + if !ok { + break + } + + // Edge case: Apparently GOROOT has vendoring for its own packages. + if v.isVendor { + if err := enqueueRootEntries(v.path, "", queue); err != nil { + return nil, err + } + + continue + } + + result, err := traverseScanEntry(v, queue) + if err != nil { + return nil, fmt.Errorf("error while scanning package %q: %w", v.importPath, err) + } + + if result == nil { + continue + } + + // Edge case: "builtin" package exists only for documentation purposes + // and not importable. + // Also skip empty packages (usually part of vendor path). + if result.pkgInfo.ImportPath != docutil.BuiltinPackage && len(result.symbols) != 0 { + packages = append(packages, result.pkgInfo) + } + + symbols = append(symbols, result.symbols...) + } + + return &GoIndexFile{ + Version: GoIndexFileVersion, + Go: goVersion, + Packages: packages, + Symbols: symbols, + }, nil +} + +type traverseResult struct { + pkgInfo PackageInfo + symbols []SymbolInfo +} + +func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*traverseResult, error) { + var ( + pkgInfo PackageInfo + symbols []SymbolInfo + ) + + dirents, err := os.ReadDir(entry.path) + if err != nil { + return nil, fmt.Errorf("can't read dir %q: %w", entry.path, err) + } + + fset := token.NewFileSet() + for _, dirent := range dirents { + name := dirent.Name() + absPath := filepath.Join(entry.path, name) + + if !dirent.IsDir() { + if strings.HasSuffix(name, "_test.go") || !strings.HasSuffix(name, ".go") { + continue + } + + f, err := parseFile(fset, absPath, fileParseParams{ + parseDoc: pkgInfo.Documentation != "", + importPath: entry.importPath, + }) + if err != nil { + return nil, fmt.Errorf("can't parse file %q: %w", absPath, err) + } + + symbols = append(symbols, f.symbols...) + pkgInfo.Name = f.packageName + if f.doc != nil { + pkgInfo.Documentation = docutil.BuildPackageDoc(f.doc, entry.importPath) + } + continue + } + + if isDirIgnored(name) { + continue + } + + // TODO: should nested vendors be supported? + if imports.IsVendorDir(name) { + queue.Add(scanEntry{ + isVendor: true, + path: absPath, + }) + continue + } + + queue.Add(scanEntry{ + path: absPath, + importPath: path.Join(), + }) + } + + return &traverseResult{ + pkgInfo: pkgInfo, + symbols: symbols, + }, nil +} + +func enqueueRootEntries(rootDir string, parentImportPath string, queue *imports.Queue[scanEntry]) error { + entries, err := os.ReadDir(rootDir) + if err != nil { + return fmt.Errorf("can't read dir %q: %w", rootDir, err) + } + + for _, entry := range entries { + if !entry.IsDir() || isDirIgnored(entry.Name()) { + continue + } + + absPath := filepath.Join(rootDir, entry.Name()) + if imports.IsVendorDir(entry.Name()) { + queue.Add(scanEntry{ + isVendor: true, + path: absPath, + }) + continue + } + + importPath := entry.Name() + if parentImportPath != "" { + importPath = path.Join(parentImportPath, importPath) + } + queue.Add(scanEntry{ + path: absPath, + importPath: entry.Name(), + }) + } + + return nil +} + +func isDirIgnored(basename string) bool { + switch basename { + case "cmd", "internal", "testdata": + return true + } + + return false +} diff --git a/internal/pkgindex/index/types.go b/internal/pkgindex/index/types.go new file mode 100644 index 00000000..6170ad77 --- /dev/null +++ b/internal/pkgindex/index/types.go @@ -0,0 +1,83 @@ +package index + +import "github.com/x1unix/go-playground/pkg/monaco" + +const GoIndexFileVersion = 1 + +type PackageInfo struct { + // Name is package name. + Name string `json:"name"` + + // ImportPath is full import path of a package. + ImportPath string `json:"importPath"` + + // Documentation is documentation in Markdown format. + Documentation string `json:"documentation"` +} + +type SymbolInfo struct { + // Name is symbol name. + Name string `json:"name"` + + // Documentation is documentation in Markdown format. + Documentation string `json:"documentation"` + + // Detail is symbol summary. + Detail string `json:"detail"` + + // InsertText is text to be inserted by completion. + InsertText string `json:"insertText"` + + // InsertTextRules controls InsertText snippet format. + InsertTextRules monaco.CompletionItemInsertTextRule `json:"insertTextRules,omitempty"` + + // Kind is symbol type. + Kind monaco.CompletionItemKind `json:"kind"` + + // Package contains information where symbol came from. + Package SymbolSource `json:"package"` +} + +func SymbolInfoFromCompletionItem(item monaco.CompletionItem, src SymbolSource) SymbolInfo { + doc := item.Documentation.String + if item.Documentation.Value != nil { + doc = item.Documentation.Value.Value + } + + label := item.Label.String + if item.Label.Value != nil { + label = item.Label.Value.Label + } + + return SymbolInfo{ + Name: label, + Documentation: doc, + Detail: item.Detail, + InsertText: item.InsertText, + InsertTextRules: item.InsertTextRules, + Kind: item.Kind, + Package: src, + } +} + +type SymbolSource struct { + // Name is package name. + Name string `json:"name"` + + // Path is import path of a package. + Path string `json:"path"` +} + +type GoIndexFile struct { + // Version is file format version. + Version int `json:"version"` + + // Go is Go version used to generate index. + Go string `json:"go"` + + // Packages is list of standard Go packages. + Packages []PackageInfo `json:"packages"` + + // Symbols is list of all Go symbols. + Symbols []SymbolInfo `json:"symbols"` +} diff --git a/internal/pkgindex/queue.go b/internal/pkgindex/queue.go deleted file mode 100644 index 81498128..00000000 --- a/internal/pkgindex/queue.go +++ /dev/null @@ -1,31 +0,0 @@ -package pkgindex - -type queue struct { - entries []string -} - -func newQueue(size int) queue { - return queue{entries: make([]string, 0, size)} -} - -func (q *queue) occupied() bool { - return len(q.entries) != 0 -} - -func (q *queue) pop() (string, bool) { - if len(q.entries) == 0 { - return "", false - } - - entry := q.entries[0] - q.entries = q.entries[1:] - return entry, true -} - -func (q *queue) add(items ...string) { - if len(items) == 0 { - return - } - - q.entries = append(q.entries, items...) -} diff --git a/tools/pkgindexer/main.go b/tools/pkgindexer/main.go index 91b452f1..68648d11 100644 --- a/tools/pkgindexer/main.go +++ b/tools/pkgindexer/main.go @@ -3,27 +3,45 @@ package main import ( "encoding/json" "fmt" + "github.com/x1unix/go-playground/internal/pkgindex/index" "io" "log" "os" "path/filepath" "github.com/spf13/cobra" - "github.com/x1unix/go-playground/internal/pkgindex" + "github.com/x1unix/go-playground/internal/pkgindex/imports" ) type Flags struct { goRoot string outFile string prettyPrint bool + stdout bool +} + +func (f Flags) Validate() error { + if f.outFile == "" && !f.stdout { + return fmt.Errorf("missing output file flag. Use --stdout flag to print into stdout") + } + + if f.stdout && f.outFile != "" { + return fmt.Errorf("ambiguous output flag: --stdout and output file flag can't be together") + } + + return nil } func (f Flags) WithDefaults() (Flags, error) { + if err := f.Validate(); err != nil { + return f, err + } + if f.goRoot != "" { return f, nil } - goRoot, err := pkgindex.ResolveGoRoot() + goRoot, err := imports.ResolveGoRoot() if err != nil { return f, fmt.Errorf( "cannot find GOROOT, please set GOROOT path or check if Go is installed.\nError: %w", @@ -38,54 +56,72 @@ func main() { var flags Flags cmd := &cobra.Command{ SilenceUsage: true, - Use: "pkgindexer [-r goroot] [-o output]", + Use: "pkgindexer [-r goroot] [-o output]", Short: "Go standard library packages scanner", Long: "Tool to generate Go package autocomplete entries for Monaco editor from Go SDK", - RunE: func(cmd *cobra.Command, args []string) error { + } + + cmd.AddCommand(&cobra.Command{ + Use: "imports [-r goroot] [-o output]", + Short: "Generate imports.json file for old Playground version", + Long: "Generate imports file which contains list of all importable packages. Used in legacy app versions", + RunE: func(_ *cobra.Command, _ []string) error { resolvedFlags, err := flags.WithDefaults() if err != nil { return err } - return runErr(resolvedFlags) + return runGenImports(resolvedFlags) }, - } + }) + + cmd.AddCommand(&cobra.Command{ + Use: "index [-r goroot] [-o output]", + Short: "Generate index file with standard Go packages and symbols", + Long: "Generate a JSON file that contains list of all standard Go packages and its symbols. Used in new version of app", + RunE: func(_ *cobra.Command, _ []string) error { + resolvedFlags, err := flags.WithDefaults() + if err != nil { + return err + } - cmd.PersistentFlags().StringVarP(&flags.goRoot, "root", "r", "", "Path to GOROOT. Uses $GOROOT by default.") - cmd.PersistentFlags().StringVarP(&flags.outFile, "output", "o", "", "Path to output file. When enpty, prints to stdout.") + return runGenIndex(resolvedFlags) + }, + }) + + cmd.PersistentFlags().StringVarP(&flags.goRoot, "root", "r", "", "Path to GOROOT. Uses $GOROOT by default") + cmd.PersistentFlags().StringVarP(&flags.outFile, "output", "o", "", "Path to output file. When enpty, prints to stdout") cmd.PersistentFlags().BoolVarP(&flags.prettyPrint, "pretty", "P", false, "Add indents to JSON output") + cmd.PersistentFlags().BoolVar(&flags.stdout, "stdout", false, "Dump result into stdout") if err := cmd.Execute(); err != nil { os.Exit(2) } } -func runErr(flags Flags) error { - scanner := pkgindex.NewGoRootScanner(flags.goRoot) - results, err := scanner.Scan() +func runGenIndex(flags Flags) error { + entries, err := index.ScanRoot(flags.goRoot) if err != nil { return err } - if flags.outFile == "" { - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - return getEncoder(os.Stdout, flags.prettyPrint).Encode(results) - } - - if err := os.MkdirAll(filepath.Dir(flags.outFile), 0755); err != nil { - return fmt.Errorf("failed to pre-create parent directories: %w", err) + if err := writeOutput(flags, entries); err != nil { + return err } - f, err := os.OpenFile(flags.outFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) - defer silentClose(f) + log.Printf("Scanned %d packages", len(entries.Packages)) + return nil +} +func runGenImports(flags Flags) error { + scanner := imports.NewGoRootScanner(flags.goRoot) + results, err := scanner.Scan() if err != nil { - return fmt.Errorf("can't create output file: %w", err) + return err } - if err := getEncoder(f, flags.prettyPrint).Encode(results); err != nil { - return fmt.Errorf("can't write JSON to file %q: %w", flags.outFile, err) + if err := writeOutput(flags, results); err != nil { + return err } log.Printf("Scanned %d packages", len(results.Packages)) @@ -101,6 +137,29 @@ func getEncoder(dst io.Writer, pretty bool) *json.Encoder { return enc } +func writeOutput(flags Flags, data any) error { + if flags.outFile == "" { + return getEncoder(os.Stdout, true).Encode(data) + } + + if err := os.MkdirAll(filepath.Dir(flags.outFile), 0755); err != nil { + return fmt.Errorf("failed to pre-create parent directories: %w", err) + } + + f, err := os.OpenFile(flags.outFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + defer silentClose(f) + + if err != nil { + return fmt.Errorf("can't create output file: %w", err) + } + + if err := getEncoder(f, flags.prettyPrint).Encode(data); err != nil { + return fmt.Errorf("can't write JSON to file %q: %w", flags.outFile, err) + } + + return nil +} + func silentClose(c io.Closer) { // I don't care _ = c.Close() diff --git a/web/src/services/completion/types.ts b/web/src/services/completion/types.ts index c1fca1c8..eb130e90 100644 --- a/web/src/services/completion/types.ts +++ b/web/src/services/completion/types.ts @@ -92,6 +92,7 @@ export interface SymbolInfo { documentation: string detail: string insertText: string + insertTextRules: monaco.languages.CompletionItemInsertTextRule kind: monaco.languages.CompletionItemKind package: { name: string @@ -99,6 +100,11 @@ export interface SymbolInfo { } } +/** + * Go index file response type. + * + * Should be in sync with `/internal/pkgindex/index/types.go`! + */ export interface GoIndexFile { /** * File format version. From 5001a0033e7a597b05639dbc0ce70b79a68c2d51 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 17:31:37 -0400 Subject: [PATCH 07/43] fix: fix comment validator --- internal/pkgindex/docutil/comments.go | 11 ++---- internal/pkgindex/docutil/comments_test.go | 42 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 internal/pkgindex/docutil/comments_test.go diff --git a/internal/pkgindex/docutil/comments.go b/internal/pkgindex/docutil/comments.go index fdca45c3..c1dae0fd 100644 --- a/internal/pkgindex/docutil/comments.go +++ b/internal/pkgindex/docutil/comments.go @@ -99,15 +99,8 @@ func hasPackagePrefix(c string) bool { return false } - pfx := []rune(pkgDocPrefix) - for i, char := range c[len(pkgDocPrefix):] { - char = unicode.ToLower(char) - if char != pfx[i] { - return false - } - } - - return true + pfx := c[:len(pkgDocPrefix)] + return strings.EqualFold(pfx, pkgDocPrefix) } // isDirective reports whether c is a comment directive. diff --git a/internal/pkgindex/docutil/comments_test.go b/internal/pkgindex/docutil/comments_test.go new file mode 100644 index 00000000..6afd9dc2 --- /dev/null +++ b/internal/pkgindex/docutil/comments_test.go @@ -0,0 +1,42 @@ +package docutil + +import ( + "go/ast" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsPackageDoc(t *testing.T) { + cases := map[string]struct { + expect bool + group *ast.CommentGroup + }{ + "bufio": { + expect: true, + group: &ast.CommentGroup{ + List: []*ast.Comment{ + { + Slash: 161, + Text: "// Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer", + }, + { + Slash: 238, + Text: "// object, creating another object (Reader or Writer) that also implements", + }, + { + Slash: 313, + Text: "// the interface but provides buffering and some help for textual I/O.", + }, + }, + }, + }, + } + + for n, c := range cases { + t.Run(n, func(t *testing.T) { + got := IsPackageDoc(c.group) + require.Equal(t, c.expect, got) + }) + } +} From 3343682e5d336d5f37cdbb55ca4b69fa605ed7c6 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 17:32:03 -0400 Subject: [PATCH 08/43] fix: fix package filter --- internal/pkgindex/docutil/type.go | 2 +- internal/pkgindex/index/scanner.go | 78 +++----------------------- internal/pkgindex/index/traverse.go | 87 +++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 71 deletions(-) create mode 100644 internal/pkgindex/index/traverse.go diff --git a/internal/pkgindex/docutil/type.go b/internal/pkgindex/docutil/type.go index 6b8d28f5..af5adc18 100644 --- a/internal/pkgindex/docutil/type.go +++ b/internal/pkgindex/docutil/type.go @@ -70,7 +70,7 @@ func TypeToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.TypeSp if !isPrimitive { signature, err := DeclToString(fset, block.Decl) if err != nil { - return item, fmt.Errorf("%w (type: %q, pos: %s)", err, item.Label, GetDeclPosition(fset, spec)) + return item, fmt.Errorf("%w (type: %q, pos: %s)", err, item.Label.String, GetDeclPosition(fset, spec)) } item.Detail = signature diff --git a/internal/pkgindex/index/scanner.go b/internal/pkgindex/index/scanner.go index 5be30b4c..94c51813 100644 --- a/internal/pkgindex/index/scanner.go +++ b/internal/pkgindex/index/scanner.go @@ -2,7 +2,6 @@ package index import ( "fmt" - "go/token" "log" "os" "path" @@ -77,7 +76,7 @@ func ScanRoot(goRoot string) (*GoIndexFile, error) { // Edge case: "builtin" package exists only for documentation purposes // and not importable. // Also skip empty packages (usually part of vendor path). - if result.pkgInfo.ImportPath != docutil.BuiltinPackage && len(result.symbols) != 0 { + if result.pkgInfo.ImportPath != docutil.BuiltinPackage && len(result.symbols) > 0 { packages = append(packages, result.pkgInfo) } @@ -92,73 +91,6 @@ func ScanRoot(goRoot string) (*GoIndexFile, error) { }, nil } -type traverseResult struct { - pkgInfo PackageInfo - symbols []SymbolInfo -} - -func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*traverseResult, error) { - var ( - pkgInfo PackageInfo - symbols []SymbolInfo - ) - - dirents, err := os.ReadDir(entry.path) - if err != nil { - return nil, fmt.Errorf("can't read dir %q: %w", entry.path, err) - } - - fset := token.NewFileSet() - for _, dirent := range dirents { - name := dirent.Name() - absPath := filepath.Join(entry.path, name) - - if !dirent.IsDir() { - if strings.HasSuffix(name, "_test.go") || !strings.HasSuffix(name, ".go") { - continue - } - - f, err := parseFile(fset, absPath, fileParseParams{ - parseDoc: pkgInfo.Documentation != "", - importPath: entry.importPath, - }) - if err != nil { - return nil, fmt.Errorf("can't parse file %q: %w", absPath, err) - } - - symbols = append(symbols, f.symbols...) - pkgInfo.Name = f.packageName - if f.doc != nil { - pkgInfo.Documentation = docutil.BuildPackageDoc(f.doc, entry.importPath) - } - continue - } - - if isDirIgnored(name) { - continue - } - - // TODO: should nested vendors be supported? - if imports.IsVendorDir(name) { - queue.Add(scanEntry{ - isVendor: true, - path: absPath, - }) - continue - } - - queue.Add(scanEntry{ - path: absPath, - importPath: path.Join(), - }) - } - - return &traverseResult{ - pkgInfo: pkgInfo, - symbols: symbols, - }, nil -} - func enqueueRootEntries(rootDir string, parentImportPath string, queue *imports.Queue[scanEntry]) error { entries, err := os.ReadDir(rootDir) if err != nil { @@ -166,7 +98,7 @@ func enqueueRootEntries(rootDir string, parentImportPath string, queue *imports. } for _, entry := range entries { - if !entry.IsDir() || isDirIgnored(entry.Name()) { + if !entry.IsDir() || isDirIgnored(entry.Name()) || isImportPathIgnored(entry.Name()) { continue } @@ -194,9 +126,15 @@ func enqueueRootEntries(rootDir string, parentImportPath string, queue *imports. func isDirIgnored(basename string) bool { switch basename { + // Arena experiment was rejected and removed case "cmd", "internal", "testdata": return true } return false } + +func isImportPathIgnored(importPath string) bool { + // Arena experiment was rejected and removed + return importPath == "arena" +} diff --git a/internal/pkgindex/index/traverse.go b/internal/pkgindex/index/traverse.go new file mode 100644 index 00000000..7d996eed --- /dev/null +++ b/internal/pkgindex/index/traverse.go @@ -0,0 +1,87 @@ +package index + +import ( + "fmt" + "go/token" + "os" + "path" + "path/filepath" + "strings" + + "github.com/x1unix/go-playground/internal/pkgindex/docutil" + "github.com/x1unix/go-playground/internal/pkgindex/imports" +) + +type traverseResult struct { + pkgInfo PackageInfo + symbols []SymbolInfo +} + +func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*traverseResult, error) { + var ( + pkgInfo PackageInfo + symbols []SymbolInfo + ) + + dirents, err := os.ReadDir(entry.path) + if err != nil { + return nil, fmt.Errorf("can't read dir %q: %w", entry.path, err) + } + + fset := token.NewFileSet() + for _, dirent := range dirents { + name := dirent.Name() + absPath := filepath.Join(entry.path, name) + + if !dirent.IsDir() { + if strings.HasSuffix(name, "_test.go") || !strings.HasSuffix(name, ".go") { + continue + } + + f, err := parseFile(fset, absPath, fileParseParams{ + parseDoc: pkgInfo.Documentation == "", + importPath: entry.importPath, + }) + if err != nil { + return nil, fmt.Errorf("can't parse file %q: %w", absPath, err) + } + + symbols = append(symbols, f.symbols...) + pkgInfo.Name = f.packageName + if f.doc != nil { + pkgInfo.Documentation = docutil.BuildPackageDoc(f.doc, entry.importPath) + } + continue + } + + if isDirIgnored(name) { + continue + } + + // TODO: should nested vendors be supported? + if imports.IsVendorDir(name) { + queue.Add(scanEntry{ + isVendor: true, + path: absPath, + }) + continue + } + + p := path.Join(entry.importPath, name) + if !isImportPathIgnored(p) { + queue.Add(scanEntry{ + path: absPath, + importPath: p, + }) + } + } + + if len(symbols) == 0 { + return nil, nil + } + + return &traverseResult{ + pkgInfo: pkgInfo, + symbols: symbols, + }, nil +} From 9a66a7affe28a7ea8b4f4fd64f52eae27d1d9a32 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 21:56:28 -0400 Subject: [PATCH 09/43] feat: update response model --- internal/pkgindex/index/types.go | 12 ++++++------ pkg/monaco/union.go | 9 +++++++-- web/src/services/completion/types.ts | 6 +++--- web/src/services/storage/types/completion.ts | 4 ++-- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/internal/pkgindex/index/types.go b/internal/pkgindex/index/types.go index 6170ad77..0e05fcf1 100644 --- a/internal/pkgindex/index/types.go +++ b/internal/pkgindex/index/types.go @@ -11,19 +11,19 @@ type PackageInfo struct { // ImportPath is full import path of a package. ImportPath string `json:"importPath"` - // Documentation is documentation in Markdown format. - Documentation string `json:"documentation"` + // Doc is documentation in Markdown format. + Doc string `json:"doc,omitempty"` } type SymbolInfo struct { // Name is symbol name. Name string `json:"name"` - // Documentation is documentation in Markdown format. - Documentation string `json:"documentation"` + // Doc is documentation in Markdown format. + Doc string `json:"doc,omitempty"` // Detail is symbol summary. - Detail string `json:"detail"` + Detail string `json:"detail,omitempty"` // InsertText is text to be inserted by completion. InsertText string `json:"insertText"` @@ -51,7 +51,7 @@ func SymbolInfoFromCompletionItem(item monaco.CompletionItem, src SymbolSource) return SymbolInfo{ Name: label, - Documentation: doc, + Doc: doc, Detail: item.Detail, InsertText: item.InsertText, InsertTextRules: item.InsertTextRules, diff --git a/pkg/monaco/union.go b/pkg/monaco/union.go index 4665cdda..caa59536 100644 --- a/pkg/monaco/union.go +++ b/pkg/monaco/union.go @@ -82,9 +82,14 @@ func (u *UnionString[T]) UnmarshalJSON(data []byte) error { } var dst any - if data[0] == '"' { + switch data[0] { + case '"', '\'': dst = &u.String - } else { + case 'n', 'u': + // null or undefined + return nil + default: + u.Value = new(T) dst = u.Value } diff --git a/web/src/services/completion/types.ts b/web/src/services/completion/types.ts index eb130e90..226f4d04 100644 --- a/web/src/services/completion/types.ts +++ b/web/src/services/completion/types.ts @@ -84,13 +84,13 @@ export interface SuggestionQuery { export interface PackageInfo { name: string importPath: string - documentation: string + doc?: string } export interface SymbolInfo { name: string - documentation: string - detail: string + doc?: string + detail?: string insertText: string insertTextRules: monaco.languages.CompletionItemInsertTextRule kind: monaco.languages.CompletionItemKind diff --git a/web/src/services/storage/types/completion.ts b/web/src/services/storage/types/completion.ts index 1262a3a2..90e6b3ac 100644 --- a/web/src/services/storage/types/completion.ts +++ b/web/src/services/storage/types/completion.ts @@ -8,7 +8,7 @@ export type CompletionItems = monaco.languages.CompletionItem[] */ export interface NormalizedCompletionItem extends Omit { label: string - documentation: monaco.IMarkdownString + documentation?: monaco.IMarkdownString } /** @@ -33,7 +33,7 @@ export interface PackageIndexItem { /** * Inherited from CompletionItem. */ - documentation: monaco.IMarkdownString + documentation?: monaco.IMarkdownString } /** From de9d8c0e7da0416ec77dbf723f022a0585e09197 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 21:56:43 -0400 Subject: [PATCH 10/43] feat: debug parser --- .../pkgindex/docutil/testdata/bufio/bufio.go | 54 +++ .../docutil/testdata/builtin/builtin.go | 310 +++++++++++++ .../docutil/testdata/builtin/expect.json | 429 ++++++++++++++++++ .../docutil/testdata/simple/expect.json | 33 ++ .../pkgindex/docutil/testdata/simple/types.go | 14 + internal/pkgindex/docutil/traverse_test.go | 100 ++++ internal/pkgindex/docutil/utils.go | 12 + internal/pkgindex/docutil/value.go | 109 ++++- internal/pkgindex/index/traverse.go | 16 +- web/src/services/completion/utils.ts | 24 +- 10 files changed, 1058 insertions(+), 43 deletions(-) create mode 100644 internal/pkgindex/docutil/testdata/bufio/bufio.go create mode 100644 internal/pkgindex/docutil/testdata/builtin/builtin.go create mode 100644 internal/pkgindex/docutil/testdata/builtin/expect.json create mode 100644 internal/pkgindex/docutil/testdata/simple/expect.json create mode 100644 internal/pkgindex/docutil/testdata/simple/types.go create mode 100644 internal/pkgindex/docutil/traverse_test.go diff --git a/internal/pkgindex/docutil/testdata/bufio/bufio.go b/internal/pkgindex/docutil/testdata/bufio/bufio.go new file mode 100644 index 00000000..d4a15074 --- /dev/null +++ b/internal/pkgindex/docutil/testdata/bufio/bufio.go @@ -0,0 +1,54 @@ +// Package bufio is a test case package +package bufio + +import ( + "errors" + "io" +) + +const ( + defaultBufSize = 4096 +) + +var ( + ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte") + ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune") + ErrBufferFull = errors.New("bufio: buffer full") + ErrNegativeCount = errors.New("bufio: negative count") + Bar = 32 + Foo int32 + Baz bool = false +) + +// Buffered input. + +// Reader implements buffering for an io.Reader object. +type Reader struct { + buf []byte + rd io.Reader // reader provided by the client + r, w int // buf read and write positions + err error + lastByte int // last byte read for UnreadByte; -1 means invalid + lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid +} + +func (_ Reader) Read(_ []byte) (int, error) { + return 0, io.EOF +} + +const minReadBufferSize = 16 +const maxConsecutiveEmptyReads = 100 + +// NewReaderSize returns a new [Reader] whose buffer has at least the specified +// size. If the argument io.Reader is already a [Reader] with large enough +// size, it returns the underlying [Reader]. +func NewReaderSize(rd io.Reader, size int) *Reader { + // Is it already a Reader? + b, ok := rd.(*Reader) + if ok && len(b.buf) >= size { + return b + } + r := new(Reader) + //r.reset(make([]byte, max(size, minReadBufferSize)), rd) + return r +} diff --git a/internal/pkgindex/docutil/testdata/builtin/builtin.go b/internal/pkgindex/docutil/testdata/builtin/builtin.go new file mode 100644 index 00000000..215c59c4 --- /dev/null +++ b/internal/pkgindex/docutil/testdata/builtin/builtin.go @@ -0,0 +1,310 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package builtin provides documentation for Go's predeclared identifiers. +The items documented here are not actually in package builtin +but their descriptions here allow godoc to present documentation +for the language's special identifiers. +*/ +package builtin + +import "cmp" + +// bool is the set of boolean values, true and false. +type bool bool + +// true and false are the two untyped boolean values. +const ( + true = 0 == 0 // Untyped bool. + false = 0 != 0 // Untyped bool. +) + +// uint8 is the set of all unsigned 8-bit integers. +// Range: 0 through 255. +type uint8 uint8 + +// uint16 is the set of all unsigned 16-bit integers. +// Range: 0 through 65535. +type uint16 uint16 + +// uint32 is the set of all unsigned 32-bit integers. +// Range: 0 through 4294967295. +type uint32 uint32 + +// uint64 is the set of all unsigned 64-bit integers. +// Range: 0 through 18446744073709551615. +type uint64 uint64 + +// int8 is the set of all signed 8-bit integers. +// Range: -128 through 127. +type int8 int8 + +// int16 is the set of all signed 16-bit integers. +// Range: -32768 through 32767. +type int16 int16 + +// int32 is the set of all signed 32-bit integers. +// Range: -2147483648 through 2147483647. +type int32 int32 + +// int64 is the set of all signed 64-bit integers. +// Range: -9223372036854775808 through 9223372036854775807. +type int64 int64 + +// float32 is the set of all IEEE 754 32-bit floating-point numbers. +type float32 float32 + +// float64 is the set of all IEEE 754 64-bit floating-point numbers. +type float64 float64 + +// complex64 is the set of all complex numbers with float32 real and +// imaginary parts. +type complex64 complex64 + +// complex128 is the set of all complex numbers with float64 real and +// imaginary parts. +type complex128 complex128 + +// string is the set of all strings of 8-bit bytes, conventionally but not +// necessarily representing UTF-8-encoded text. A string may be empty, but +// not nil. Values of string type are immutable. +type string string + +// int is a signed integer type that is at least 32 bits in size. It is a +// distinct type, however, and not an alias for, say, int32. +type int int + +// uint is an unsigned integer type that is at least 32 bits in size. It is a +// distinct type, however, and not an alias for, say, uint32. +type uint uint + +// uintptr is an integer type that is large enough to hold the bit pattern of +// any pointer. +type uintptr uintptr + +// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is +// used, by convention, to distinguish byte values from 8-bit unsigned +// integer values. +type byte = uint8 + +// rune is an alias for int32 and is equivalent to int32 in all ways. It is +// used, by convention, to distinguish character values from integer values. +type rune = int32 + +// any is an alias for interface{} and is equivalent to interface{} in all ways. +type any = interface{} + +// comparable is an interface that is implemented by all comparable types +// (booleans, numbers, strings, pointers, channels, arrays of comparable types, +// structs whose fields are all comparable types). +// The comparable interface may only be used as a type parameter constraint, +// not as the type of a variable. +type comparable interface{ comparable } + +// iota is a predeclared identifier representing the untyped integer ordinal +// number of the current const specification in a (usually parenthesized) +// const declaration. It is zero-indexed. +const iota = 0 // Untyped int. + +// nil is a predeclared identifier representing the zero value for a +// pointer, channel, func, interface, map, or slice type. +var nil Type // Type must be a pointer, channel, func, interface, map, or slice type + +// Type is here for the purposes of documentation only. It is a stand-in +// for any Go type, but represents the same type for any given function +// invocation. +type Type int + +// Type1 is here for the purposes of documentation only. It is a stand-in +// for any Go type, but represents the same type for any given function +// invocation. +type Type1 int + +// IntegerType is here for the purposes of documentation only. It is a stand-in +// for any integer type: int, uint, int8 etc. +type IntegerType int + +// FloatType is here for the purposes of documentation only. It is a stand-in +// for either float type: float32 or float64. +type FloatType float32 + +// ComplexType is here for the purposes of documentation only. It is a +// stand-in for either complex type: complex64 or complex128. +type ComplexType complex64 + +// The append built-in function appends elements to the end of a slice. If +// it has sufficient capacity, the destination is resliced to accommodate the +// new elements. If it does not, a new underlying array will be allocated. +// Append returns the updated slice. It is therefore necessary to store the +// result of append, often in the variable holding the slice itself: +// +// slice = append(slice, elem1, elem2) +// slice = append(slice, anotherSlice...) +// +// As a special case, it is legal to append a string to a byte slice, like this: +// +// slice = append([]byte("hello "), "world"...) +func append(slice []Type, elems ...Type) []Type + +// The copy built-in function copies elements from a source slice into a +// destination slice. (As a special case, it also will copy bytes from a +// string to a slice of bytes.) The source and destination may overlap. Copy +// returns the number of elements copied, which will be the minimum of +// len(src) and len(dst). +func copy(dst, src []Type) int + +// The delete built-in function deletes the element with the specified key +// (m[key]) from the map. If m is nil or there is no such element, delete +// is a no-op. +func delete(m map[Type]Type1, key Type) + +// The len built-in function returns the length of v, according to its type: +// +// Array: the number of elements in v. +// Pointer to array: the number of elements in *v (even if v is nil). +// Slice, or map: the number of elements in v; if v is nil, len(v) is zero. +// String: the number of bytes in v. +// Channel: the number of elements queued (unread) in the channel buffer; +// if v is nil, len(v) is zero. +// +// For some arguments, such as a string literal or a simple array expression, the +// result can be a constant. See the Go language specification's "Length and +// capacity" section for details. +func len(v Type) int + +// The cap built-in function returns the capacity of v, according to its type: +// +// Array: the number of elements in v (same as len(v)). +// Pointer to array: the number of elements in *v (same as len(v)). +// Slice: the maximum length the slice can reach when resliced; +// if v is nil, cap(v) is zero. +// Channel: the channel buffer capacity, in units of elements; +// if v is nil, cap(v) is zero. +// +// For some arguments, such as a simple array expression, the result can be a +// constant. See the Go language specification's "Length and capacity" section for +// details. +func cap(v Type) int + +// The make built-in function allocates and initializes an object of type +// slice, map, or chan (only). Like new, the first argument is a type, not a +// value. Unlike new, make's return type is the same as the type of its +// argument, not a pointer to it. The specification of the result depends on +// the type: +// +// Slice: The size specifies the length. The capacity of the slice is +// equal to its length. A second integer argument may be provided to +// specify a different capacity; it must be no smaller than the +// length. For example, make([]int, 0, 10) allocates an underlying array +// of size 10 and returns a slice of length 0 and capacity 10 that is +// backed by this underlying array. +// Map: An empty map is allocated with enough space to hold the +// specified number of elements. The size may be omitted, in which case +// a small starting size is allocated. +// Channel: The channel's buffer is initialized with the specified +// buffer capacity. If zero, or the size is omitted, the channel is +// unbuffered. +func make(t Type, size ...IntegerType) Type + +// The max built-in function returns the largest value of a fixed number of +// arguments of [cmp.Ordered] types. There must be at least one argument. +// If T is a floating-point type and any of the arguments are NaNs, +// max will return NaN. +func max[T cmp.Ordered](x T, y ...T) T + +// The min built-in function returns the smallest value of a fixed number of +// arguments of [cmp.Ordered] types. There must be at least one argument. +// If T is a floating-point type and any of the arguments are NaNs, +// min will return NaN. +func min[T cmp.Ordered](x T, y ...T) T + +// The new built-in function allocates memory. The first argument is a type, +// not a value, and the value returned is a pointer to a newly +// allocated zero value of that type. +func new(Type) *Type + +// The complex built-in function constructs a complex value from two +// floating-point values. The real and imaginary parts must be of the same +// size, either float32 or float64 (or assignable to them), and the return +// value will be the corresponding complex type (complex64 for float32, +// complex128 for float64). +func complex(r, i FloatType) ComplexType + +// The real built-in function returns the real part of the complex number c. +// The return value will be floating point type corresponding to the type of c. +func real(c ComplexType) FloatType + +// The imag built-in function returns the imaginary part of the complex +// number c. The return value will be floating point type corresponding to +// the type of c. +func imag(c ComplexType) FloatType + +// The clear built-in function clears maps and slices. +// For maps, clear deletes all entries, resulting in an empty map. +// For slices, clear sets all elements up to the length of the slice +// to the zero value of the respective element type. If the argument +// type is a type parameter, the type parameter's type set must +// contain only map or slice types, and clear performs the operation +// implied by the type argument. +func clear[T ~[]Type | ~map[Type]Type1](t T) + +// The close built-in function closes a channel, which must be either +// bidirectional or send-only. It should be executed only by the sender, +// never the receiver, and has the effect of shutting down the channel after +// the last sent value is received. After the last value has been received +// from a closed channel c, any receive from c will succeed without +// blocking, returning the zero value for the channel element. The form +// +// x, ok := <-c +// +// will also set ok to false for a closed and empty channel. +func close(c chan<- Type) + +// The panic built-in function stops normal execution of the current +// goroutine. When a function F calls panic, normal execution of F stops +// immediately. Any functions whose execution was deferred by F are run in +// the usual way, and then F returns to its caller. To the caller G, the +// invocation of F then behaves like a call to panic, terminating G's +// execution and running any deferred functions. This continues until all +// functions in the executing goroutine have stopped, in reverse order. At +// that point, the program is terminated with a non-zero exit code. This +// termination sequence is called panicking and can be controlled by the +// built-in function recover. +// +// Starting in Go 1.21, calling panic with a nil interface value or an +// untyped nil causes a run-time error (a different panic). +// The GODEBUG setting panicnil=1 disables the run-time error. +func panic(v any) + +// The recover built-in function allows a program to manage behavior of a +// panicking goroutine. Executing a call to recover inside a deferred +// function (but not any function called by it) stops the panicking sequence +// by restoring normal execution and retrieves the error value passed to the +// call of panic. If recover is called outside the deferred function it will +// not stop a panicking sequence. In this case, or when the goroutine is not +// panicking, recover returns nil. +// +// Prior to Go 1.21, recover would also return nil if panic is called with +// a nil argument. See [panic] for details. +func recover() any + +// The print built-in function formats its arguments in an +// implementation-specific way and writes the result to standard error. +// Print is useful for bootstrapping and debugging; it is not guaranteed +// to stay in the language. +func print(args ...Type) + +// The println built-in function formats its arguments in an +// implementation-specific way and writes the result to standard error. +// Spaces are always added between arguments and a newline is appended. +// Println is useful for bootstrapping and debugging; it is not guaranteed +// to stay in the language. +func println(args ...Type) + +// The error built-in interface type is the conventional interface for +// representing an error condition, with the nil value representing no error. +type error interface { + Error() string +} diff --git a/internal/pkgindex/docutil/testdata/builtin/expect.json b/internal/pkgindex/docutil/testdata/builtin/expect.json new file mode 100644 index 00000000..cef46475 --- /dev/null +++ b/internal/pkgindex/docutil/testdata/builtin/expect.json @@ -0,0 +1,429 @@ +[ + { + "label": "bool", + "documentation": { + "value": "" + }, + "insertText": "bool", + "kind": 5 + }, + { + "label": "true", + "documentation": null, + "detail": "const true = 0 == 0", + "insertText": "true", + "kind": 14 + }, + { + "label": "false", + "documentation": null, + "detail": "const false = 0 != 0", + "insertText": "false", + "kind": 14 + }, + { + "label": "uint8", + "documentation": { + "value": "" + }, + "insertText": "uint8", + "kind": 5 + }, + { + "label": "uint16", + "documentation": { + "value": "" + }, + "insertText": "uint16", + "kind": 5 + }, + { + "label": "uint32", + "documentation": { + "value": "" + }, + "insertText": "uint32", + "kind": 5 + }, + { + "label": "uint64", + "documentation": { + "value": "" + }, + "insertText": "uint64", + "kind": 5 + }, + { + "label": "int8", + "documentation": { + "value": "" + }, + "insertText": "int8", + "kind": 5 + }, + { + "label": "int16", + "documentation": { + "value": "" + }, + "insertText": "int16", + "kind": 5 + }, + { + "label": "int32", + "documentation": { + "value": "" + }, + "insertText": "int32", + "kind": 5 + }, + { + "label": "int64", + "documentation": { + "value": "" + }, + "insertText": "int64", + "kind": 5 + }, + { + "label": "float32", + "documentation": { + "value": "" + }, + "insertText": "float32", + "kind": 5 + }, + { + "label": "float64", + "documentation": { + "value": "" + }, + "insertText": "float64", + "kind": 5 + }, + { + "label": "complex64", + "documentation": { + "value": "" + }, + "insertText": "complex64", + "kind": 5 + }, + { + "label": "complex128", + "documentation": { + "value": "" + }, + "insertText": "complex128", + "kind": 5 + }, + { + "label": "string", + "documentation": { + "value": "" + }, + "insertText": "string", + "kind": 5 + }, + { + "label": "int", + "documentation": { + "value": "" + }, + "insertText": "int", + "kind": 5 + }, + { + "label": "uint", + "documentation": { + "value": "" + }, + "insertText": "uint", + "kind": 5 + }, + { + "label": "uintptr", + "documentation": { + "value": "" + }, + "insertText": "uintptr", + "kind": 5 + }, + { + "label": "byte", + "documentation": { + "value": "" + }, + "insertText": "byte", + "kind": 5 + }, + { + "label": "rune", + "documentation": { + "value": "" + }, + "insertText": "rune", + "kind": 5 + }, + { + "label": "any", + "documentation": { + "value": "" + }, + "detail": "type any = interface{}", + "insertText": "any", + "kind": 7 + }, + { + "label": "comparable", + "documentation": { + "value": "" + }, + "detail": "type comparable interface{ comparable }", + "insertText": "comparable", + "kind": 7 + }, + { + "label": "iota", + "documentation": null, + "detail": "const iota = 0", + "insertText": "iota", + "kind": 14 + }, + { + "label": "nil", + "documentation": null, + "detail": "var nil Type", + "insertText": "nil", + "kind": 4 + }, + { + "label": "Type", + "documentation": { + "value": "" + }, + "insertText": "Type", + "kind": 5 + }, + { + "label": "Type1", + "documentation": { + "value": "" + }, + "insertText": "Type1", + "kind": 5 + }, + { + "label": "IntegerType", + "documentation": { + "value": "" + }, + "insertText": "IntegerType", + "kind": 5 + }, + { + "label": "FloatType", + "documentation": { + "value": "" + }, + "insertText": "FloatType", + "kind": 5 + }, + { + "label": "ComplexType", + "documentation": { + "value": "" + }, + "insertText": "ComplexType", + "kind": 5 + }, + { + "label": "append", + "documentation": { + "value": "The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice. It is therefore necessary to store the result of append, often in the variable holding the slice itself:\n\n\tslice = append(slice, elem1, elem2)\n\tslice = append(slice, anotherSlice...)\n\nAs a special case, it is legal to append a string to a byte slice, like this:\n\n\tslice = append([]byte(\"hello \"), \"world\"...)" + }, + "detail": "func append(slice []Type, elems ...Type) []Type", + "insertText": "append(${1:slice}, ${2:elems})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "copy", + "documentation": { + "value": "The copy built-in function copies elements from a source slice into a destination slice. (As a special case, it also will copy bytes from a string to a slice of bytes.) The source and destination may overlap. Copy returns the number of elements copied, which will be the minimum of len(src) and len(dst)." + }, + "detail": "func copy(dst, src []Type) int", + "insertText": "copy(${1:dst}, ${2:src})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "delete", + "documentation": { + "value": "The delete built-in function deletes the element with the specified key (m\\[key]) from the map. If m is nil or there is no such element, delete is a no-op." + }, + "detail": "func delete(m map[Type]Type1, key Type)", + "insertText": "delete(${1:m}, ${2:key})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "len", + "documentation": { + "value": "The len built-in function returns the length of v, according to its type:\n\n\tArray: the number of elements in v.\n\tPointer to array: the number of elements in *v (even if v is nil).\n\tSlice, or map: the number of elements in v; if v is nil, len(v) is zero.\n\tString: the number of bytes in v.\n\tChannel: the number of elements queued (unread) in the channel buffer;\n\t if v is nil, len(v) is zero.\n\nFor some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification's \"Length and capacity\" section for details." + }, + "detail": "func len(v Type) int", + "insertText": "len(${1:v})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "cap", + "documentation": { + "value": "The cap built-in function returns the capacity of v, according to its type:\n\n\tArray: the number of elements in v (same as len(v)).\n\tPointer to array: the number of elements in *v (same as len(v)).\n\tSlice: the maximum length the slice can reach when resliced;\n\tif v is nil, cap(v) is zero.\n\tChannel: the channel buffer capacity, in units of elements;\n\tif v is nil, cap(v) is zero.\n\nFor some arguments, such as a simple array expression, the result can be a constant. See the Go language specification's \"Length and capacity\" section for details." + }, + "detail": "func cap(v Type) int", + "insertText": "cap(${1:v})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "make", + "documentation": { + "value": "The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make's return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type:\n\n\tSlice: The size specifies the length. The capacity of the slice is\n\tequal to its length. A second integer argument may be provided to\n\tspecify a different capacity; it must be no smaller than the\n\tlength. For example, make([]int, 0, 10) allocates an underlying array\n\tof size 10 and returns a slice of length 0 and capacity 10 that is\n\tbacked by this underlying array.\n\tMap: An empty map is allocated with enough space to hold the\n\tspecified number of elements. The size may be omitted, in which case\n\ta small starting size is allocated.\n\tChannel: The channel's buffer is initialized with the specified\n\tbuffer capacity. If zero, or the size is omitted, the channel is\n\tunbuffered." + }, + "detail": "func make(t Type, size ...IntegerType) Type", + "insertText": "make(${1:t}, ${2:size})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "max", + "documentation": { + "value": "The max built-in function returns the largest value of a fixed number of arguments of [cmp.Ordered](/cmp#Ordered) types. There must be at least one argument. If T is a floating-point type and any of the arguments are NaNs, max will return NaN." + }, + "detail": "func max[T cmp.Ordered](x T, y ...T) T", + "insertText": "max[${1:T}](${2:x}, ${3:y})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "min", + "documentation": { + "value": "The min built-in function returns the smallest value of a fixed number of arguments of [cmp.Ordered](/cmp#Ordered) types. There must be at least one argument. If T is a floating-point type and any of the arguments are NaNs, min will return NaN." + }, + "detail": "func min[T cmp.Ordered](x T, y ...T) T", + "insertText": "min[${1:T}](${2:x}, ${3:y})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "new", + "documentation": { + "value": "The new built-in function allocates memory. The first argument is a type, not a value, and the value returned is a pointer to a newly allocated zero value of that type." + }, + "detail": "func new(Type) *Type", + "insertText": "new()", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "complex", + "documentation": { + "value": "The complex built-in function constructs a complex value from two floating-point values. The real and imaginary parts must be of the same size, either float32 or float64 (or assignable to them), and the return value will be the corresponding complex type (complex64 for float32, complex128 for float64)." + }, + "detail": "func complex(r, i FloatType) ComplexType", + "insertText": "complex(${1:r}, ${2:i})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "real", + "documentation": { + "value": "The real built-in function returns the real part of the complex number c. The return value will be floating point type corresponding to the type of c." + }, + "detail": "func real(c ComplexType) FloatType", + "insertText": "real(${1:c})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "imag", + "documentation": { + "value": "The imag built-in function returns the imaginary part of the complex number c. The return value will be floating point type corresponding to the type of c." + }, + "detail": "func imag(c ComplexType) FloatType", + "insertText": "imag(${1:c})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "clear", + "documentation": { + "value": "The clear built-in function clears maps and slices. For maps, clear deletes all entries, resulting in an empty map. For slices, clear sets all elements up to the length of the slice to the zero value of the respective element type. If the argument type is a type parameter, the type parameter's type set must contain only map or slice types, and clear performs the operation implied by the type argument." + }, + "detail": "func clear[T ~[]Type | ~map[Type]Type1](t T)", + "insertText": "clear[${1:T}](${2:t})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "close", + "documentation": { + "value": "The close built-in function closes a channel, which must be either bidirectional or send-only. It should be executed only by the sender, never the receiver, and has the effect of shutting down the channel after the last sent value is received. After the last value has been received from a closed channel c, any receive from c will succeed without blocking, returning the zero value for the channel element. The form\n\n\tx, ok := \u003c-c\n\nwill also set ok to false for a closed and empty channel." + }, + "detail": "func close(c chan\u003c- Type)", + "insertText": "close(${1:c})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "panic", + "documentation": { + "value": "The panic built-in function stops normal execution of the current goroutine. When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic, terminating G's execution and running any deferred functions. This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated with a non-zero exit code. This termination sequence is called panicking and can be controlled by the built-in function recover.\n\nStarting in Go 1.21, calling panic with a nil interface value or an untyped nil causes a run-time error (a different panic). The GODEBUG setting panicnil=1 disables the run-time error." + }, + "detail": "func panic(v any)", + "insertText": "panic(${1:v})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "recover", + "documentation": { + "value": "The recover built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, recover returns nil.\n\nPrior to Go 1.21, recover would also return nil if panic is called with a nil argument. See \\[panic] for details." + }, + "detail": "func recover() any", + "insertText": "recover()", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "print", + "documentation": { + "value": "The print built-in function formats its arguments in an implementation-specific way and writes the result to standard error. Print is useful for bootstrapping and debugging; it is not guaranteed to stay in the language." + }, + "detail": "func print(args ...Type)", + "insertText": "print(${1:args})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "println", + "documentation": { + "value": "The println built-in function formats its arguments in an implementation-specific way and writes the result to standard error. Spaces are always added between arguments and a newline is appended. Println is useful for bootstrapping and debugging; it is not guaranteed to stay in the language." + }, + "detail": "func println(args ...Type)", + "insertText": "println(${1:args})", + "insertTextRules": 4, + "kind": 1 + }, + { + "label": "error", + "documentation": { + "value": "" + }, + "detail": "type error interface {\n\tError() string\n}", + "insertText": "error", + "kind": 7 + } +] diff --git a/internal/pkgindex/docutil/testdata/simple/expect.json b/internal/pkgindex/docutil/testdata/simple/expect.json new file mode 100644 index 00000000..0120c31c --- /dev/null +++ b/internal/pkgindex/docutil/testdata/simple/expect.json @@ -0,0 +1,33 @@ +[ + { + "label": "ErrInvalidUnreadByte", + "documentation": { + "value": "ErrInvalidUnreadByte occurs when invalid use of UnreadByte is done." + }, + "insertText": "ErrInvalidUnreadByte", + "kind": 4 + }, + { + "label": "Bar", + "documentation": null, + "detail": "var Bar = 32", + "insertText": "Bar", + "kind": 4 + }, + { + "label": "Foo", + "documentation": null, + "detail": "var Foo int32", + "insertText": "Foo", + "kind": 4 + }, + { + "label": "Baz", + "documentation": { + "value": "Baz is a sample value." + }, + "detail": "var Baz bool", + "insertText": "Baz", + "kind": 4 + } +] diff --git a/internal/pkgindex/docutil/testdata/simple/types.go b/internal/pkgindex/docutil/testdata/simple/types.go new file mode 100644 index 00000000..30270d1c --- /dev/null +++ b/internal/pkgindex/docutil/testdata/simple/types.go @@ -0,0 +1,14 @@ +// Package simple is a test package. +package simple + +import "errors" + +var ( + // ErrInvalidUnreadByte occurs when invalid use of UnreadByte is done. + ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte") + Bar = 32 + Foo int32 + + // Baz is a sample value. + Baz bool = false +) diff --git a/internal/pkgindex/docutil/traverse_test.go b/internal/pkgindex/docutil/traverse_test.go new file mode 100644 index 00000000..63f265a5 --- /dev/null +++ b/internal/pkgindex/docutil/traverse_test.go @@ -0,0 +1,100 @@ +package docutil + +import ( + "encoding/json" + "go/parser" + "go/token" + "golang.org/x/exp/slices" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/x1unix/go-playground/pkg/monaco" +) + +func TestTypeToCompletionItem(t *testing.T) { + cases := map[string]struct { + filePath string + expectFile string + expectErr string + dumpOnly bool + allowUnexported bool + }{ + //"bufio": { + // filePath: "testdata/bufio/bufio.go", + //}, + //"simple": { + // filePath: "testdata/simple/types.go", + // expectFile: "testdata/simple/expect.json", + //}, + "builtin": { + dumpOnly: true, + allowUnexported: true, + filePath: "testdata/builtin/builtin.go", + expectFile: "testdata/builtin/expect.json", + }, + } + + for n, c := range cases { + t.Run(n, func(t *testing.T) { + fset := token.NewFileSet() + + r, err := parser.ParseFile(fset, c.filePath, nil, parser.ParseComments) + require.NoError(t, err) + + opts := TraverseOpts{ + AllowUnexported: c.allowUnexported, + FileSet: fset, + SnippetFormat: monaco.InsertAsSnippet, + } + + var ( + got []monaco.CompletionItem + want []monaco.CompletionItem + ) + err = CollectCompletionItems(r.Decls, opts, func(items ...monaco.CompletionItem) { + got = append(got, items...) + }) + if c.expectErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), c.expectErr) + return + } + + require.NoError(t, err) + + if c.dumpOnly { + // for debug + dumpCompletionItems(t, got, c.expectFile) + return + } + + f, err := os.Open(c.expectFile) + require.NoError(t, err) + t.Cleanup(func() { + _ = f.Close() + }) + + require.NoError(t, json.NewDecoder(f).Decode(&want)) + slices.SortFunc(got, func(a, b monaco.CompletionItem) bool { + return strings.Compare(a.Label.String, b.Label.String) == -1 + }) + slices.SortFunc(want, func(a, b monaco.CompletionItem) bool { + return strings.Compare(a.Label.String, b.Label.String) == -1 + }) + + require.Equal(t, want, got) + }) + } +} + +func dumpCompletionItems(t *testing.T, items []monaco.CompletionItem, dst string) { + f, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + require.NoError(t, err) + defer f.Close() + + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + _ = enc.Encode(items) +} diff --git a/internal/pkgindex/docutil/utils.go b/internal/pkgindex/docutil/utils.go index 5649f1ca..27c98c1f 100644 --- a/internal/pkgindex/docutil/utils.go +++ b/internal/pkgindex/docutil/utils.go @@ -33,6 +33,18 @@ func TokenToCompletionItemKind(tok token.Token) (monaco.CompletionItemKind, bool return k, ok } +// IsGoSourceFile returns whether a file name is a Go source file. +// +// Returns false for unit test files (this is intentional behavior). +func IsGoSourceFile(name string) bool { + return strings.HasSuffix(name, "_test.go") || !strings.HasSuffix(name, ".go") +} + +// CommentGroupEmpty checks whether passed command group is empty. +func CommentGroupEmpty(g *ast.CommentGroup) bool { + return g == nil || len(g.List) == 0 +} + // GetDeclRange returns AST node range in document. func GetDeclRange(fset *token.FileSet, decl ast.Decl) (start token.Position, end token.Position) { f := fset.File(decl.Pos()) diff --git a/internal/pkgindex/docutil/value.go b/internal/pkgindex/docutil/value.go index a6fd0cbd..c72c7d96 100644 --- a/internal/pkgindex/docutil/value.go +++ b/internal/pkgindex/docutil/value.go @@ -12,12 +12,7 @@ import ( // // Able to handle special edge cases for builtin declarations. func ValueToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.ValueSpec, allowUnexported bool) ([]monaco.CompletionItem, error) { - var blockDoc *monaco.IMarkdownString - if !block.IsGroup { - blockDoc = &monaco.IMarkdownString{ - Value: string(FormatCommentGroup(spec.Doc)), - } - } + blockDoc := getValueDocumentation(block, spec) items := make([]monaco.CompletionItem, 0, len(spec.Values)) for _, val := range spec.Names { @@ -25,35 +20,99 @@ func ValueToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.Value continue } + detail, err := detailFromIdent(fset, block, val) + if err != nil { + return nil, err + } + item := monaco.CompletionItem{ Kind: block.Kind, InsertText: val.Name, + Detail: detail, } item.Label.String = val.Name item.Documentation.SetValue(blockDoc) + items = append(items, item) + } + + return items, nil +} + +func getValueDocumentation(block BlockData, spec *ast.ValueSpec) *monaco.IMarkdownString { + commentGroup := block.Decl.Doc + if !block.IsGroup || len(block.Decl.Specs) > 1 || CommentGroupEmpty(commentGroup) { + commentGroup = spec.Doc + } + + if CommentGroupEmpty(commentGroup) { + return nil + } + + return &monaco.IMarkdownString{ + Value: string(FormatCommentGroup(spec.Doc)), + } +} + +func detailFromIdent(fset *token.FileSet, block BlockData, ident *ast.Ident) (string, error) { + valDecl, ok := isIdentPrintable(ident) + if !ok { + return "", nil + } - switch val.Name { - case "true", "false": - // TODO: handle builtins - default: - signature, err := DeclToString(fset, val.Obj.Decl) - if err != nil { - return nil, fmt.Errorf( - "%w (value name: %s, pos: %s)", err, val.Name, GetDeclPosition(fset, val), - ) - } - - // declaration type is not present in value block. - if signature != "" { - signature = block.Decl.Tok.String() + " " + signature - } - - item.Detail = signature + signature, err := DeclToString(fset, valDecl) + if err != nil { + return "", fmt.Errorf( + "%w (value name: %s, pos: %s)", err, ident.Name, GetDeclPosition(fset, ident), + ) + } + + if signature != "" { + signature = block.Decl.Tok.String() + " " + signature + } + + return signature, nil +} + +// isIdentPrintable checks whether completion item detail can be +// generated from identifier. +// +// Returns stripped, save to print version of identifier value on success. +// +// Returns true if identifier has type definition, scalar value or is from "builtin" package. +func isIdentPrintable(ident *ast.Ident) (any, bool) { + switch ident.Name { + case "true", "false": + // Edge case: builtin package. + return ident.Obj.Decl, true + } + + // Permit only value declarations + val, ok := ident.Obj.Decl.(*ast.ValueSpec) + if !ok { + return nil, false + } + + // Allow identifier with type + isScalar := true + for _, spec := range val.Values { + if _, ok := spec.(*ast.BasicLit); !ok { + isScalar = false + break } + } - items = append(items, item) + if isScalar { + return ident.Obj.Decl, true } - return items, nil + // Allow typed identifiers, but strip value + // Return type should be a pointer in order to satisfy printer interfaces. + if val.Type != nil { + modifiedVal := *val + modifiedVal.Values = nil + return &modifiedVal, true + } + + return nil, false } diff --git a/internal/pkgindex/index/traverse.go b/internal/pkgindex/index/traverse.go index 7d996eed..c1e2d241 100644 --- a/internal/pkgindex/index/traverse.go +++ b/internal/pkgindex/index/traverse.go @@ -2,14 +2,12 @@ package index import ( "fmt" + "github.com/x1unix/go-playground/internal/pkgindex/docutil" + "github.com/x1unix/go-playground/internal/pkgindex/imports" "go/token" "os" "path" "path/filepath" - "strings" - - "github.com/x1unix/go-playground/internal/pkgindex/docutil" - "github.com/x1unix/go-playground/internal/pkgindex/imports" ) type traverseResult struct { @@ -19,8 +17,10 @@ type traverseResult struct { func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*traverseResult, error) { var ( - pkgInfo PackageInfo symbols []SymbolInfo + pkgInfo = PackageInfo{ + ImportPath: entry.importPath, + } ) dirents, err := os.ReadDir(entry.path) @@ -34,12 +34,12 @@ func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*trave absPath := filepath.Join(entry.path, name) if !dirent.IsDir() { - if strings.HasSuffix(name, "_test.go") || !strings.HasSuffix(name, ".go") { + if !docutil.IsGoSourceFile(name) { continue } f, err := parseFile(fset, absPath, fileParseParams{ - parseDoc: pkgInfo.Documentation == "", + parseDoc: pkgInfo.Doc == "", importPath: entry.importPath, }) if err != nil { @@ -49,7 +49,7 @@ func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*trave symbols = append(symbols, f.symbols...) pkgInfo.Name = f.packageName if f.doc != nil { - pkgInfo.Documentation = docutil.BuildPackageDoc(f.doc, entry.importPath) + pkgInfo.Doc = docutil.BuildPackageDoc(f.doc, entry.importPath) } continue } diff --git a/web/src/services/completion/utils.ts b/web/src/services/completion/utils.ts index 7ce15859..ff79015a 100644 --- a/web/src/services/completion/utils.ts +++ b/web/src/services/completion/utils.ts @@ -12,20 +12,22 @@ const stubRange = undefined as any as monaco.IRange const packageCompletionKind = 8 -export const intoPackageIndexItem = ({ name, importPath, documentation }: PackageInfo): PackageIndexItem => ({ +export const intoPackageIndexItem = ({ name, importPath, doc }: PackageInfo): PackageIndexItem => ({ importPath, name, prefix: getPrefix(name), - documentation: { - value: documentation, - isTrusted: true, - }, + documentation: doc + ? { + value: doc, + isTrusted: true, + } + : undefined, }) export const intoSymbolIndexItem = ({ name, package: pkg, - documentation, + doc, ...completion }: SymbolInfo): SymbolIndexItem => ({ ...completion, @@ -34,10 +36,12 @@ export const intoSymbolIndexItem = ({ label: name, packageName: pkg.name, packagePath: pkg.path, - documentation: { - value: documentation, - isTrusted: true, - }, + documentation: doc + ? { + value: doc, + isTrusted: true, + } + : undefined, }) export const importCompletionFromPackage = ({ importPath, name, documentation }: PackageIndexItem): CompletionItem => ({ From 0bb2f5b2eb50f03850e501becf39f7be7ccab422 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 22:15:05 -0400 Subject: [PATCH 11/43] fix: fix doc generation --- internal/pkgindex/docutil/type.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/internal/pkgindex/docutil/type.go b/internal/pkgindex/docutil/type.go index af5adc18..7e2e6c35 100644 --- a/internal/pkgindex/docutil/type.go +++ b/internal/pkgindex/docutil/type.go @@ -44,10 +44,7 @@ func NewBlockData(specGroup *ast.GenDecl) (BlockData, error) { // TypeToCompletionItem returns completion item from type declaration inside block. func TypeToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.TypeSpec) (monaco.CompletionItem, error) { // Block declarations contain doc inside each child. - declCommentGroup := spec.Comment - if block.IsGroup { - declCommentGroup = block.Decl.Doc - } + doc := getTypeDoc(block, spec) item := monaco.CompletionItem{ Kind: block.Kind, @@ -76,9 +73,22 @@ func TypeToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.TypeSp item.Detail = signature } - item.Documentation.SetValue(&monaco.IMarkdownString{ - Value: string(FormatCommentGroup(declCommentGroup)), - }) - + item.Documentation.SetValue(doc) return item, nil } + +func getTypeDoc(block BlockData, spec *ast.TypeSpec) *monaco.IMarkdownString { + g := block.Decl.Doc + if block.IsGroup || len(block.Decl.Specs) > 1 { + // standalone type declarations are still considered as block. + g = spec.Doc + } + + if CommentGroupEmpty(g) { + return nil + } + + return &monaco.IMarkdownString{ + Value: string(FormatCommentGroup(g)), + } +} From d029738d45fd117545d0897309a54026ceb6c446 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 22:20:25 -0400 Subject: [PATCH 12/43] fix: fix doc gen for types --- .../docutil/testdata/builtin/expect.json | 62 ++++++++++--------- internal/pkgindex/docutil/value.go | 10 +-- 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/internal/pkgindex/docutil/testdata/builtin/expect.json b/internal/pkgindex/docutil/testdata/builtin/expect.json index cef46475..0a3a57dd 100644 --- a/internal/pkgindex/docutil/testdata/builtin/expect.json +++ b/internal/pkgindex/docutil/testdata/builtin/expect.json @@ -2,7 +2,7 @@ { "label": "bool", "documentation": { - "value": "" + "value": "bool is the set of boolean values, true and false." }, "insertText": "bool", "kind": 5 @@ -24,7 +24,7 @@ { "label": "uint8", "documentation": { - "value": "" + "value": "uint8 is the set of all unsigned 8-bit integers. Range: 0 through 255." }, "insertText": "uint8", "kind": 5 @@ -32,7 +32,7 @@ { "label": "uint16", "documentation": { - "value": "" + "value": "uint16 is the set of all unsigned 16-bit integers. Range: 0 through 65535." }, "insertText": "uint16", "kind": 5 @@ -40,7 +40,7 @@ { "label": "uint32", "documentation": { - "value": "" + "value": "uint32 is the set of all unsigned 32-bit integers. Range: 0 through 4294967295." }, "insertText": "uint32", "kind": 5 @@ -48,7 +48,7 @@ { "label": "uint64", "documentation": { - "value": "" + "value": "uint64 is the set of all unsigned 64-bit integers. Range: 0 through 18446744073709551615." }, "insertText": "uint64", "kind": 5 @@ -56,7 +56,7 @@ { "label": "int8", "documentation": { - "value": "" + "value": "int8 is the set of all signed 8-bit integers. Range: -128 through 127." }, "insertText": "int8", "kind": 5 @@ -64,7 +64,7 @@ { "label": "int16", "documentation": { - "value": "" + "value": "int16 is the set of all signed 16-bit integers. Range: -32768 through 32767." }, "insertText": "int16", "kind": 5 @@ -72,7 +72,7 @@ { "label": "int32", "documentation": { - "value": "" + "value": "int32 is the set of all signed 32-bit integers. Range: -2147483648 through 2147483647." }, "insertText": "int32", "kind": 5 @@ -80,7 +80,7 @@ { "label": "int64", "documentation": { - "value": "" + "value": "int64 is the set of all signed 64-bit integers. Range: -9223372036854775808 through 9223372036854775807." }, "insertText": "int64", "kind": 5 @@ -88,7 +88,7 @@ { "label": "float32", "documentation": { - "value": "" + "value": "float32 is the set of all IEEE 754 32-bit floating-point numbers." }, "insertText": "float32", "kind": 5 @@ -96,7 +96,7 @@ { "label": "float64", "documentation": { - "value": "" + "value": "float64 is the set of all IEEE 754 64-bit floating-point numbers." }, "insertText": "float64", "kind": 5 @@ -104,7 +104,7 @@ { "label": "complex64", "documentation": { - "value": "" + "value": "complex64 is the set of all complex numbers with float32 real and imaginary parts." }, "insertText": "complex64", "kind": 5 @@ -112,7 +112,7 @@ { "label": "complex128", "documentation": { - "value": "" + "value": "complex128 is the set of all complex numbers with float64 real and imaginary parts." }, "insertText": "complex128", "kind": 5 @@ -120,7 +120,7 @@ { "label": "string", "documentation": { - "value": "" + "value": "string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable." }, "insertText": "string", "kind": 5 @@ -128,7 +128,7 @@ { "label": "int", "documentation": { - "value": "" + "value": "int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32." }, "insertText": "int", "kind": 5 @@ -136,7 +136,7 @@ { "label": "uint", "documentation": { - "value": "" + "value": "uint is an unsigned integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, uint32." }, "insertText": "uint", "kind": 5 @@ -144,7 +144,7 @@ { "label": "uintptr", "documentation": { - "value": "" + "value": "uintptr is an integer type that is large enough to hold the bit pattern of any pointer." }, "insertText": "uintptr", "kind": 5 @@ -152,7 +152,7 @@ { "label": "byte", "documentation": { - "value": "" + "value": "byte is an alias for uint8 and is equivalent to uint8 in all ways. It is used, by convention, to distinguish byte values from 8-bit unsigned integer values." }, "insertText": "byte", "kind": 5 @@ -160,7 +160,7 @@ { "label": "rune", "documentation": { - "value": "" + "value": "rune is an alias for int32 and is equivalent to int32 in all ways. It is used, by convention, to distinguish character values from integer values." }, "insertText": "rune", "kind": 5 @@ -168,7 +168,7 @@ { "label": "any", "documentation": { - "value": "" + "value": "any is an alias for interface{} and is equivalent to interface{} in all ways." }, "detail": "type any = interface{}", "insertText": "any", @@ -177,7 +177,7 @@ { "label": "comparable", "documentation": { - "value": "" + "value": "comparable is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels, arrays of comparable types, structs whose fields are all comparable types). The comparable interface may only be used as a type parameter constraint, not as the type of a variable." }, "detail": "type comparable interface{ comparable }", "insertText": "comparable", @@ -185,14 +185,18 @@ }, { "label": "iota", - "documentation": null, + "documentation": { + "value": "iota is a predeclared identifier representing the untyped integer ordinal number of the current const specification in a (usually parenthesized) const declaration. It is zero-indexed." + }, "detail": "const iota = 0", "insertText": "iota", "kind": 14 }, { "label": "nil", - "documentation": null, + "documentation": { + "value": "nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type." + }, "detail": "var nil Type", "insertText": "nil", "kind": 4 @@ -200,7 +204,7 @@ { "label": "Type", "documentation": { - "value": "" + "value": "Type is here for the purposes of documentation only. It is a stand-in for any Go type, but represents the same type for any given function invocation." }, "insertText": "Type", "kind": 5 @@ -208,7 +212,7 @@ { "label": "Type1", "documentation": { - "value": "" + "value": "Type1 is here for the purposes of documentation only. It is a stand-in for any Go type, but represents the same type for any given function invocation." }, "insertText": "Type1", "kind": 5 @@ -216,7 +220,7 @@ { "label": "IntegerType", "documentation": { - "value": "" + "value": "IntegerType is here for the purposes of documentation only. It is a stand-in for any integer type: int, uint, int8 etc." }, "insertText": "IntegerType", "kind": 5 @@ -224,7 +228,7 @@ { "label": "FloatType", "documentation": { - "value": "" + "value": "FloatType is here for the purposes of documentation only. It is a stand-in for either float type: float32 or float64." }, "insertText": "FloatType", "kind": 5 @@ -232,7 +236,7 @@ { "label": "ComplexType", "documentation": { - "value": "" + "value": "ComplexType is here for the purposes of documentation only. It is a stand-in for either complex type: complex64 or complex128." }, "insertText": "ComplexType", "kind": 5 @@ -420,7 +424,7 @@ { "label": "error", "documentation": { - "value": "" + "value": "The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error." }, "detail": "type error interface {\n\tError() string\n}", "insertText": "error", diff --git a/internal/pkgindex/docutil/value.go b/internal/pkgindex/docutil/value.go index c72c7d96..5b307c51 100644 --- a/internal/pkgindex/docutil/value.go +++ b/internal/pkgindex/docutil/value.go @@ -40,17 +40,17 @@ func ValueToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.Value } func getValueDocumentation(block BlockData, spec *ast.ValueSpec) *monaco.IMarkdownString { - commentGroup := block.Decl.Doc - if !block.IsGroup || len(block.Decl.Specs) > 1 || CommentGroupEmpty(commentGroup) { - commentGroup = spec.Doc + g := block.Decl.Doc + if block.IsGroup || len(block.Decl.Specs) > 1 || CommentGroupEmpty(g) { + g = spec.Doc } - if CommentGroupEmpty(commentGroup) { + if CommentGroupEmpty(g) { return nil } return &monaco.IMarkdownString{ - Value: string(FormatCommentGroup(spec.Doc)), + Value: string(FormatCommentGroup(g)), } } From 57f2eaf77bf3856187a992853133187d32a2be56 Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 23:38:19 -0400 Subject: [PATCH 13/43] feat: add filter for builtins --- go.mod | 5 +- go.sum | 2 + internal/pkgindex/docutil/decl.go | 17 ++++--- internal/pkgindex/docutil/filter.go | 59 ++++++++++++++++++++++ internal/pkgindex/docutil/func.go | 2 +- internal/pkgindex/docutil/traverse.go | 11 ++-- internal/pkgindex/docutil/traverse_test.go | 36 +++++++------ internal/pkgindex/docutil/value.go | 19 ++++--- internal/pkgindex/index/parse.go | 18 +++++-- 9 files changed, 129 insertions(+), 40 deletions(-) create mode 100644 internal/pkgindex/docutil/filter.go diff --git a/go.mod b/go.mod index 72c884a8..d706c0b1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/x1unix/go-playground -go 1.21 +go 1.23 + +toolchain go1.23.1 require ( github.com/TheZeroSlave/zapsentry v1.10.0 @@ -25,6 +27,7 @@ require ( github.com/benbjohnson/clock v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/getsentry/sentry-go v0.13.0 // indirect + github.com/hashicorp/go-set/v3 v3.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/cobra v1.8.1 // indirect diff --git a/go.sum b/go.sum index eadbbca0..cb8fbba4 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6 github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA= +github.com/hashicorp/go-set/v3 v3.0.0/go.mod h1:IEghM2MpE5IaNvL+D7X480dfNtxjRXZ6VMpK3C8s2ok= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= diff --git a/internal/pkgindex/docutil/decl.go b/internal/pkgindex/docutil/decl.go index 73fb604b..5eb371a1 100644 --- a/internal/pkgindex/docutil/decl.go +++ b/internal/pkgindex/docutil/decl.go @@ -7,27 +7,28 @@ import ( "go/token" ) -func DeclToCompletionItem(fset *token.FileSet, specGroup *ast.GenDecl, allowUnexported bool) ([]monaco.CompletionItem, error) { +func DeclToCompletionItem(fset *token.FileSet, specGroup *ast.GenDecl, filter Filter) ([]monaco.CompletionItem, error) { if len(specGroup.Specs) == 0 { return nil, nil } + filter = filterOrDefault(filter) block, err := NewBlockData(specGroup) if err != nil { return nil, err } - // block declarations have documentation inside block child, e.g: - // var ( - // // doc - // foo = 1 - // ) + traverseCtx := TraverseContext{ + FileSet: fset, + Block: block, + Filter: filter, + } completions := make([]monaco.CompletionItem, 0, len(specGroup.Specs)) for _, spec := range specGroup.Specs { switch t := spec.(type) { case *ast.TypeSpec: - if !t.Name.IsExported() && !allowUnexported { + if filter.Ignore(t.Name.String()) { continue } @@ -38,7 +39,7 @@ func DeclToCompletionItem(fset *token.FileSet, specGroup *ast.GenDecl, allowUnex completions = append(completions, item) case *ast.ValueSpec: - items, err := ValueToCompletionItem(fset, block, t, allowUnexported) + items, err := ValueToCompletionItem(traverseCtx, t) if err != nil { return nil, err } diff --git a/internal/pkgindex/docutil/filter.go b/internal/pkgindex/docutil/filter.go new file mode 100644 index 00000000..c9721dde --- /dev/null +++ b/internal/pkgindex/docutil/filter.go @@ -0,0 +1,59 @@ +package docutil + +import ( + "go/token" + + "github.com/hashicorp/go-set/v3" +) + +// Filter is interface to provide way to ignore certain symbols during AST traversal. +type Filter interface { + Ignore(typeName string) bool +} + +type ignoreList struct { + m *set.Set[string] +} + +func (f ignoreList) Ignore(typeName string) bool { + return f.m.Contains(typeName) +} + +// NewIgnoreList creates a filter with a list of ignored symbols. +func NewIgnoreList(names ...string) Filter { + return ignoreList{ + m: set.From(names), + } +} + +// UnexportedFilter filters private symbols +type UnexportedFilter struct{} + +func (_ UnexportedFilter) Ignore(typeName string) bool { + return !token.IsExported(typeName) +} + +type composedFilter []Filter + +func (filters composedFilter) Ignore(typeName string) bool { + for _, f := range filters { + if !f.Ignore(typeName) { + return false + } + } + + return true +} + +// ComposeFilters allows composing multiple symbol filters into one. +func ComposeFilters(filters ...Filter) Filter { + return composedFilter(filters) +} + +func filterOrDefault(f Filter) Filter { + if f != nil { + return f + } + + return UnexportedFilter{} +} diff --git a/internal/pkgindex/docutil/func.go b/internal/pkgindex/docutil/func.go index f2f94e4d..d2dcc1d6 100644 --- a/internal/pkgindex/docutil/func.go +++ b/internal/pkgindex/docutil/func.go @@ -25,7 +25,7 @@ func CompletionItemFromFunc(fset *token.FileSet, fn *ast.FuncDecl, snippetFormat }) // TODO: ensure that body is removed - item.Detail, err = DeclToString(fset, fn) + item.Detail, err = PrintFuncSignature(fset, fn) if err != nil { return item, err } diff --git a/internal/pkgindex/docutil/traverse.go b/internal/pkgindex/docutil/traverse.go index 134ec971..1cd0ad42 100644 --- a/internal/pkgindex/docutil/traverse.go +++ b/internal/pkgindex/docutil/traverse.go @@ -10,19 +10,20 @@ import ( ) type TraverseOpts struct { - AllowUnexported bool - FileSet *token.FileSet - SnippetFormat monaco.CompletionItemInsertTextRule + Filter Filter + FileSet *token.FileSet + SnippetFormat monaco.CompletionItemInsertTextRule } type TraverseReducer = func(items ...monaco.CompletionItem) // CollectCompletionItems traverses root file declarations and transforms them into completion items. func CollectCompletionItems(decls []ast.Decl, opts TraverseOpts, reducer TraverseReducer) error { + filter := filterOrDefault(opts.Filter) for _, decl := range decls { switch t := decl.(type) { case *ast.FuncDecl: - if !t.Name.IsExported() && !opts.AllowUnexported { + if filter.Ignore(t.Name.String()) { continue } @@ -40,7 +41,7 @@ func CollectCompletionItems(decls []ast.Decl, opts TraverseOpts, reducer Travers continue } - items, err := DeclToCompletionItem(opts.FileSet, t, opts.AllowUnexported) + items, err := DeclToCompletionItem(opts.FileSet, t, filter) if err != nil { return fmt.Errorf( "can't parse decl %s: %w (at %s)", diff --git a/internal/pkgindex/docutil/traverse_test.go b/internal/pkgindex/docutil/traverse_test.go index 63f265a5..f63aa49d 100644 --- a/internal/pkgindex/docutil/traverse_test.go +++ b/internal/pkgindex/docutil/traverse_test.go @@ -15,24 +15,26 @@ import ( func TestTypeToCompletionItem(t *testing.T) { cases := map[string]struct { - filePath string - expectFile string - expectErr string - dumpOnly bool - allowUnexported bool + filePath string + expectFile string + expectErr string + dumpOnly bool + ignore []string }{ //"bufio": { // filePath: "testdata/bufio/bufio.go", //}, - //"simple": { - // filePath: "testdata/simple/types.go", - // expectFile: "testdata/simple/expect.json", - //}, + "simple": { + dumpOnly: true, + filePath: "testdata/simple/types.go", + expectFile: "testdata/simple/expect.json", + }, "builtin": { - dumpOnly: true, - allowUnexported: true, - filePath: "testdata/builtin/builtin.go", - expectFile: "testdata/builtin/expect.json", + ignore: []string{ + "Type", "Type1", "IntegerType", "FloatType", "ComplexType", + }, + filePath: "testdata/builtin/builtin.go", + expectFile: "testdata/builtin/expect.json", }, } @@ -44,9 +46,11 @@ func TestTypeToCompletionItem(t *testing.T) { require.NoError(t, err) opts := TraverseOpts{ - AllowUnexported: c.allowUnexported, - FileSet: fset, - SnippetFormat: monaco.InsertAsSnippet, + FileSet: fset, + SnippetFormat: monaco.InsertAsSnippet, + } + if len(c.ignore) > 0 { + opts.Filter = NewIgnoreList(c.ignore...) } var ( diff --git a/internal/pkgindex/docutil/value.go b/internal/pkgindex/docutil/value.go index 5b307c51..f3663c18 100644 --- a/internal/pkgindex/docutil/value.go +++ b/internal/pkgindex/docutil/value.go @@ -8,25 +8,32 @@ import ( "github.com/x1unix/go-playground/pkg/monaco" ) +type TraverseContext struct { + FileSet *token.FileSet + Block BlockData + Filter Filter +} + // ValueToCompletionItem constructs completion item from value declaration. // // Able to handle special edge cases for builtin declarations. -func ValueToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.ValueSpec, allowUnexported bool) ([]monaco.CompletionItem, error) { - blockDoc := getValueDocumentation(block, spec) +func ValueToCompletionItem(ctx TraverseContext, spec *ast.ValueSpec) ([]monaco.CompletionItem, error) { + filter := filterOrDefault(ctx.Filter) + blockDoc := getValueDocumentation(ctx.Block, spec) items := make([]monaco.CompletionItem, 0, len(spec.Values)) for _, val := range spec.Names { - if !val.IsExported() && !allowUnexported { + if filter.Ignore(val.Name) { continue } - detail, err := detailFromIdent(fset, block, val) + detail, err := detailFromIdent(ctx.FileSet, ctx.Block, val) if err != nil { return nil, err } item := monaco.CompletionItem{ - Kind: block.Kind, + Kind: ctx.Block.Kind, InsertText: val.Name, Detail: detail, } @@ -60,7 +67,7 @@ func detailFromIdent(fset *token.FileSet, block BlockData, ident *ast.Ident) (st return "", nil } - signature, err := DeclToString(fset, valDecl) + signature, err := PrintDecl(fset, valDecl) if err != nil { return "", fmt.Errorf( "%w (value name: %s, pos: %s)", err, ident.Name, GetDeclPosition(fset, ident), diff --git a/internal/pkgindex/index/parse.go b/internal/pkgindex/index/parse.go index c7460066..1548eecd 100644 --- a/internal/pkgindex/index/parse.go +++ b/internal/pkgindex/index/parse.go @@ -8,6 +8,10 @@ import ( "go/token" ) +var ignoreBuiltins = docutil.NewIgnoreList( + "Type", "Type1", "IntegerType", "FloatType", "ComplexType", +) + type sourceSummary struct { packageName string doc *ast.CommentGroup @@ -19,6 +23,14 @@ type fileParseParams struct { parseDoc bool } +func getFilter(importPath string) docutil.Filter { + if importPath == docutil.BuiltinPackage { + return ignoreBuiltins + } + + return docutil.UnexportedFilter{} +} + func parseFile(fset *token.FileSet, fpath string, params fileParseParams) (*sourceSummary, error) { root, err := parser.ParseFile(fset, fpath, nil, parser.ParseComments) if err != nil { @@ -42,9 +54,9 @@ func parseFile(fset *token.FileSet, fpath string, params fileParseParams) (*sour } opts := docutil.TraverseOpts{ - AllowUnexported: params.importPath == docutil.BuiltinPackage, - FileSet: fset, - SnippetFormat: monaco.InsertAsSnippet, + FileSet: fset, + Filter: getFilter(params.importPath), + SnippetFormat: monaco.InsertAsSnippet, } err = docutil.CollectCompletionItems(root.Decls, opts, func(items ...monaco.CompletionItem) { From dd0a06ef02220e48b2cd3ea86e86826572cf159d Mon Sep 17 00:00:00 2001 From: x1unix Date: Mon, 30 Sep 2024 23:38:44 -0400 Subject: [PATCH 14/43] feat: keep only func signature --- internal/pkgindex/docutil/type.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkgindex/docutil/type.go b/internal/pkgindex/docutil/type.go index 7e2e6c35..812d9d0b 100644 --- a/internal/pkgindex/docutil/type.go +++ b/internal/pkgindex/docutil/type.go @@ -65,7 +65,7 @@ func TypeToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.TypeSp } if !isPrimitive { - signature, err := DeclToString(fset, block.Decl) + signature, err := PrintDecl(fset, block.Decl) if err != nil { return item, fmt.Errorf("%w (type: %q, pos: %s)", err, item.Label.String, GetDeclPosition(fset, spec)) } From 8727c4f2991b5c1feead383ab30fb0edf0a0dbf8 Mon Sep 17 00:00:00 2001 From: x1unix Date: Tue, 1 Oct 2024 01:31:07 -0400 Subject: [PATCH 15/43] feat: finish docutil --- internal/pkgindex/docutil/decl.go | 10 +- internal/pkgindex/docutil/func.go | 32 +- .../pkgindex/docutil/testdata/bufio/bufio.go | 3 - .../docutil/testdata/bufio/expect.json | 44 ++ .../docutil/testdata/builtin/expect.json | 423 +++++++----------- .../docutil/testdata/simple/expect.json | 30 +- .../pkgindex/docutil/testdata/simple/types.go | 7 + internal/pkgindex/docutil/traverse.go | 17 +- internal/pkgindex/docutil/traverse_test.go | 25 +- internal/pkgindex/docutil/type.go | 30 +- internal/pkgindex/docutil/types.go | 25 ++ internal/pkgindex/docutil/utils.go | 4 +- internal/pkgindex/docutil/value.go | 28 +- internal/pkgindex/index/parse.go | 4 +- internal/pkgindex/index/types.go | 25 +- 15 files changed, 359 insertions(+), 348 deletions(-) create mode 100644 internal/pkgindex/docutil/testdata/bufio/expect.json create mode 100644 internal/pkgindex/docutil/types.go diff --git a/internal/pkgindex/docutil/decl.go b/internal/pkgindex/docutil/decl.go index 5eb371a1..67080eb9 100644 --- a/internal/pkgindex/docutil/decl.go +++ b/internal/pkgindex/docutil/decl.go @@ -2,12 +2,12 @@ package docutil import ( "fmt" - "github.com/x1unix/go-playground/pkg/monaco" "go/ast" "go/token" ) -func DeclToCompletionItem(fset *token.FileSet, specGroup *ast.GenDecl, filter Filter) ([]monaco.CompletionItem, error) { +// DeclToSymbol constructs symbol from generic type of value spec. +func DeclToSymbol(fset *token.FileSet, specGroup *ast.GenDecl, filter Filter) ([]Symbol, error) { if len(specGroup.Specs) == 0 { return nil, nil } @@ -24,7 +24,7 @@ func DeclToCompletionItem(fset *token.FileSet, specGroup *ast.GenDecl, filter Fi Filter: filter, } - completions := make([]monaco.CompletionItem, 0, len(specGroup.Specs)) + completions := make([]Symbol, 0, len(specGroup.Specs)) for _, spec := range specGroup.Specs { switch t := spec.(type) { case *ast.TypeSpec: @@ -32,14 +32,14 @@ func DeclToCompletionItem(fset *token.FileSet, specGroup *ast.GenDecl, filter Fi continue } - item, err := TypeToCompletionItem(fset, block, t) + item, err := TypeToSymbol(fset, block, t) if err != nil { return nil, err } completions = append(completions, item) case *ast.ValueSpec: - items, err := ValueToCompletionItem(traverseCtx, t) + items, err := ValueToSymbol(traverseCtx, t) if err != nil { return nil, err } diff --git a/internal/pkgindex/docutil/func.go b/internal/pkgindex/docutil/func.go index d2dcc1d6..e189a3d8 100644 --- a/internal/pkgindex/docutil/func.go +++ b/internal/pkgindex/docutil/func.go @@ -8,24 +8,25 @@ import ( "github.com/x1unix/go-playground/pkg/monaco" ) -// CompletionItemFromFunc constructs completion item from a function AST declaration. +// SymbolFromFunc constructs completion item from a function AST declaration. // // Function documentation is generated in Markdown format. -func CompletionItemFromFunc(fset *token.FileSet, fn *ast.FuncDecl, snippetFormat monaco.CompletionItemInsertTextRule) (item monaco.CompletionItem, err error) { +func SymbolFromFunc(fset *token.FileSet, fn *ast.FuncDecl, snippetFormat monaco.CompletionItemInsertTextRule) (item Symbol, err error) { isSnippet := snippetFormat == monaco.InsertAsSnippet - item = monaco.CompletionItem{ + item = Symbol{ + Label: fn.Name.String(), Kind: monaco.Function, InsertTextRules: snippetFormat, InsertText: buildFuncInsertStatement(fn, isSnippet), + Documentation: string(FormatCommentGroup(fn.Doc)), } - item.Label.SetString(fn.Name.String()) - item.Documentation.SetValue(&monaco.IMarkdownString{ - Value: string(FormatCommentGroup(fn.Doc)), - }) + item.Detail, err = PrintFuncAnonymous(fset, fn) + if err != nil { + return item, err + } - // TODO: ensure that body is removed - item.Detail, err = PrintFuncSignature(fset, fn) + item.Signature, err = PrintFuncPrototype(fset, fn) if err != nil { return item, err } @@ -51,3 +52,16 @@ func buildFuncInsertStatement(decl *ast.FuncDecl, asSnippet bool) string { sb.WriteString(")") return sb.String() } + +// PrintFuncPrototype returns function string representation without its body. +func PrintFuncPrototype(fset *token.FileSet, decl *ast.FuncDecl) (string, error) { + // drop body from func + fn := *decl + fn.Body = nil + return PrintDecl(fset, &fn) +} + +// PrintFuncAnonymous similar to PrintFuncPrototype, but omits function name. +func PrintFuncAnonymous(fset *token.FileSet, decl *ast.FuncDecl) (string, error) { + return PrintDecl(fset, decl.Type) +} diff --git a/internal/pkgindex/docutil/testdata/bufio/bufio.go b/internal/pkgindex/docutil/testdata/bufio/bufio.go index d4a15074..df88cc35 100644 --- a/internal/pkgindex/docutil/testdata/bufio/bufio.go +++ b/internal/pkgindex/docutil/testdata/bufio/bufio.go @@ -15,9 +15,6 @@ var ( ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune") ErrBufferFull = errors.New("bufio: buffer full") ErrNegativeCount = errors.New("bufio: negative count") - Bar = 32 - Foo int32 - Baz bool = false ) // Buffered input. diff --git a/internal/pkgindex/docutil/testdata/bufio/expect.json b/internal/pkgindex/docutil/testdata/bufio/expect.json new file mode 100644 index 00000000..543de115 --- /dev/null +++ b/internal/pkgindex/docutil/testdata/bufio/expect.json @@ -0,0 +1,44 @@ +[ + { + "label": "ErrInvalidUnreadByte", + "insertText": "ErrInvalidUnreadByte", + "kind": 4, + "insertTextRules": 0 + }, + { + "label": "ErrInvalidUnreadRune", + "insertText": "ErrInvalidUnreadRune", + "kind": 4, + "insertTextRules": 0 + }, + { + "label": "ErrBufferFull", + "insertText": "ErrBufferFull", + "kind": 4, + "insertTextRules": 0 + }, + { + "label": "ErrNegativeCount", + "insertText": "ErrNegativeCount", + "kind": 4, + "insertTextRules": 0 + }, + { + "label": "Reader", + "documentation": "Reader implements buffering for an io.Reader object.", + "detail": "struct{...}", + "insertText": "Reader{}", + "signature": "type Reader struct {\n\tbuf\t\t[]byte\n\trd\t\tio.Reader\t// reader provided by the client\n\tr, w\t\tint\t\t// buf read and write positions\n\terr\t\terror\n\tlastByte\tint\t// last byte read for UnreadByte; -1 means invalid\n\tlastRuneSize\tint\t// size of last rune read for UnreadRune; -1 means invalid\n}", + "kind": 6, + "insertTextRules": 0 + }, + { + "label": "NewReaderSize", + "documentation": "NewReaderSize returns a new \\[Reader] whose buffer has at least the specified size. If the argument io.Reader is already a \\[Reader] with large enough size, it returns the underlying \\[Reader].", + "detail": "func(rd io.Reader, size int) *Reader", + "insertText": "NewReaderSize(${1:rd}, ${2:size})", + "signature": "func NewReaderSize(rd io.Reader, size int) *Reader", + "kind": 1, + "insertTextRules": 4 + } +] diff --git a/internal/pkgindex/docutil/testdata/builtin/expect.json b/internal/pkgindex/docutil/testdata/builtin/expect.json index 0a3a57dd..be36edc1 100644 --- a/internal/pkgindex/docutil/testdata/builtin/expect.json +++ b/internal/pkgindex/docutil/testdata/builtin/expect.json @@ -1,433 +1,354 @@ [ { "label": "bool", - "documentation": { - "value": "bool is the set of boolean values, true and false." - }, + "documentation": "bool is the set of boolean values, true and false.", "insertText": "bool", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "true", - "documentation": null, "detail": "const true = 0 == 0", "insertText": "true", - "kind": 14 + "kind": 14, + "insertTextRules": 0 }, { "label": "false", - "documentation": null, "detail": "const false = 0 != 0", "insertText": "false", - "kind": 14 + "kind": 14, + "insertTextRules": 0 }, { "label": "uint8", - "documentation": { - "value": "uint8 is the set of all unsigned 8-bit integers. Range: 0 through 255." - }, + "documentation": "uint8 is the set of all unsigned 8-bit integers. Range: 0 through 255.", "insertText": "uint8", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "uint16", - "documentation": { - "value": "uint16 is the set of all unsigned 16-bit integers. Range: 0 through 65535." - }, + "documentation": "uint16 is the set of all unsigned 16-bit integers. Range: 0 through 65535.", "insertText": "uint16", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "uint32", - "documentation": { - "value": "uint32 is the set of all unsigned 32-bit integers. Range: 0 through 4294967295." - }, + "documentation": "uint32 is the set of all unsigned 32-bit integers. Range: 0 through 4294967295.", "insertText": "uint32", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "uint64", - "documentation": { - "value": "uint64 is the set of all unsigned 64-bit integers. Range: 0 through 18446744073709551615." - }, + "documentation": "uint64 is the set of all unsigned 64-bit integers. Range: 0 through 18446744073709551615.", "insertText": "uint64", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "int8", - "documentation": { - "value": "int8 is the set of all signed 8-bit integers. Range: -128 through 127." - }, + "documentation": "int8 is the set of all signed 8-bit integers. Range: -128 through 127.", "insertText": "int8", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "int16", - "documentation": { - "value": "int16 is the set of all signed 16-bit integers. Range: -32768 through 32767." - }, + "documentation": "int16 is the set of all signed 16-bit integers. Range: -32768 through 32767.", "insertText": "int16", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "int32", - "documentation": { - "value": "int32 is the set of all signed 32-bit integers. Range: -2147483648 through 2147483647." - }, + "documentation": "int32 is the set of all signed 32-bit integers. Range: -2147483648 through 2147483647.", "insertText": "int32", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "int64", - "documentation": { - "value": "int64 is the set of all signed 64-bit integers. Range: -9223372036854775808 through 9223372036854775807." - }, + "documentation": "int64 is the set of all signed 64-bit integers. Range: -9223372036854775808 through 9223372036854775807.", "insertText": "int64", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "float32", - "documentation": { - "value": "float32 is the set of all IEEE 754 32-bit floating-point numbers." - }, + "documentation": "float32 is the set of all IEEE 754 32-bit floating-point numbers.", "insertText": "float32", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "float64", - "documentation": { - "value": "float64 is the set of all IEEE 754 64-bit floating-point numbers." - }, + "documentation": "float64 is the set of all IEEE 754 64-bit floating-point numbers.", "insertText": "float64", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "complex64", - "documentation": { - "value": "complex64 is the set of all complex numbers with float32 real and imaginary parts." - }, + "documentation": "complex64 is the set of all complex numbers with float32 real and imaginary parts.", "insertText": "complex64", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "complex128", - "documentation": { - "value": "complex128 is the set of all complex numbers with float64 real and imaginary parts." - }, + "documentation": "complex128 is the set of all complex numbers with float64 real and imaginary parts.", "insertText": "complex128", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "string", - "documentation": { - "value": "string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable." - }, + "documentation": "string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable.", "insertText": "string", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "int", - "documentation": { - "value": "int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32." - }, + "documentation": "int is a signed integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, int32.", "insertText": "int", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "uint", - "documentation": { - "value": "uint is an unsigned integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, uint32." - }, + "documentation": "uint is an unsigned integer type that is at least 32 bits in size. It is a distinct type, however, and not an alias for, say, uint32.", "insertText": "uint", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "uintptr", - "documentation": { - "value": "uintptr is an integer type that is large enough to hold the bit pattern of any pointer." - }, + "documentation": "uintptr is an integer type that is large enough to hold the bit pattern of any pointer.", "insertText": "uintptr", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "byte", - "documentation": { - "value": "byte is an alias for uint8 and is equivalent to uint8 in all ways. It is used, by convention, to distinguish byte values from 8-bit unsigned integer values." - }, + "documentation": "byte is an alias for uint8 and is equivalent to uint8 in all ways. It is used, by convention, to distinguish byte values from 8-bit unsigned integer values.", "insertText": "byte", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "rune", - "documentation": { - "value": "rune is an alias for int32 and is equivalent to int32 in all ways. It is used, by convention, to distinguish character values from integer values." - }, + "documentation": "rune is an alias for int32 and is equivalent to int32 in all ways. It is used, by convention, to distinguish character values from integer values.", "insertText": "rune", - "kind": 5 + "kind": 5, + "insertTextRules": 0 }, { "label": "any", - "documentation": { - "value": "any is an alias for interface{} and is equivalent to interface{} in all ways." - }, - "detail": "type any = interface{}", + "documentation": "any is an alias for interface{} and is equivalent to interface{} in all ways.", + "detail": "interface{...}", "insertText": "any", - "kind": 7 + "signature": "type any = interface{}", + "kind": 7, + "insertTextRules": 0 }, { "label": "comparable", - "documentation": { - "value": "comparable is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels, arrays of comparable types, structs whose fields are all comparable types). The comparable interface may only be used as a type parameter constraint, not as the type of a variable." - }, - "detail": "type comparable interface{ comparable }", + "documentation": "comparable is an interface that is implemented by all comparable types (booleans, numbers, strings, pointers, channels, arrays of comparable types, structs whose fields are all comparable types). The comparable interface may only be used as a type parameter constraint, not as the type of a variable.", + "detail": "interface{...}", "insertText": "comparable", - "kind": 7 + "signature": "type comparable interface{ comparable }", + "kind": 7, + "insertTextRules": 0 }, { "label": "iota", - "documentation": { - "value": "iota is a predeclared identifier representing the untyped integer ordinal number of the current const specification in a (usually parenthesized) const declaration. It is zero-indexed." - }, + "documentation": "iota is a predeclared identifier representing the untyped integer ordinal number of the current const specification in a (usually parenthesized) const declaration. It is zero-indexed.", "detail": "const iota = 0", "insertText": "iota", - "kind": 14 + "kind": 14, + "insertTextRules": 0 }, { "label": "nil", - "documentation": { - "value": "nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type." - }, + "documentation": "nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type.", "detail": "var nil Type", "insertText": "nil", - "kind": 4 - }, - { - "label": "Type", - "documentation": { - "value": "Type is here for the purposes of documentation only. It is a stand-in for any Go type, but represents the same type for any given function invocation." - }, - "insertText": "Type", - "kind": 5 - }, - { - "label": "Type1", - "documentation": { - "value": "Type1 is here for the purposes of documentation only. It is a stand-in for any Go type, but represents the same type for any given function invocation." - }, - "insertText": "Type1", - "kind": 5 - }, - { - "label": "IntegerType", - "documentation": { - "value": "IntegerType is here for the purposes of documentation only. It is a stand-in for any integer type: int, uint, int8 etc." - }, - "insertText": "IntegerType", - "kind": 5 - }, - { - "label": "FloatType", - "documentation": { - "value": "FloatType is here for the purposes of documentation only. It is a stand-in for either float type: float32 or float64." - }, - "insertText": "FloatType", - "kind": 5 - }, - { - "label": "ComplexType", - "documentation": { - "value": "ComplexType is here for the purposes of documentation only. It is a stand-in for either complex type: complex64 or complex128." - }, - "insertText": "ComplexType", - "kind": 5 + "kind": 4, + "insertTextRules": 0 }, { "label": "append", - "documentation": { - "value": "The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice. It is therefore necessary to store the result of append, often in the variable holding the slice itself:\n\n\tslice = append(slice, elem1, elem2)\n\tslice = append(slice, anotherSlice...)\n\nAs a special case, it is legal to append a string to a byte slice, like this:\n\n\tslice = append([]byte(\"hello \"), \"world\"...)" - }, - "detail": "func append(slice []Type, elems ...Type) []Type", + "documentation": "The append built-in function appends elements to the end of a slice. If it has sufficient capacity, the destination is resliced to accommodate the new elements. If it does not, a new underlying array will be allocated. Append returns the updated slice. It is therefore necessary to store the result of append, often in the variable holding the slice itself:\n\n\tslice = append(slice, elem1, elem2)\n\tslice = append(slice, anotherSlice...)\n\nAs a special case, it is legal to append a string to a byte slice, like this:\n\n\tslice = append([]byte(\"hello \"), \"world\"...)", + "detail": "func(slice []Type, elems ...Type) []Type", "insertText": "append(${1:slice}, ${2:elems})", - "insertTextRules": 4, - "kind": 1 + "signature": "func append(slice []Type, elems ...Type) []Type", + "kind": 1, + "insertTextRules": 4 }, { "label": "copy", - "documentation": { - "value": "The copy built-in function copies elements from a source slice into a destination slice. (As a special case, it also will copy bytes from a string to a slice of bytes.) The source and destination may overlap. Copy returns the number of elements copied, which will be the minimum of len(src) and len(dst)." - }, - "detail": "func copy(dst, src []Type) int", + "documentation": "The copy built-in function copies elements from a source slice into a destination slice. (As a special case, it also will copy bytes from a string to a slice of bytes.) The source and destination may overlap. Copy returns the number of elements copied, which will be the minimum of len(src) and len(dst).", + "detail": "func(dst, src []Type) int", "insertText": "copy(${1:dst}, ${2:src})", - "insertTextRules": 4, - "kind": 1 + "signature": "func copy(dst, src []Type) int", + "kind": 1, + "insertTextRules": 4 }, { "label": "delete", - "documentation": { - "value": "The delete built-in function deletes the element with the specified key (m\\[key]) from the map. If m is nil or there is no such element, delete is a no-op." - }, - "detail": "func delete(m map[Type]Type1, key Type)", + "documentation": "The delete built-in function deletes the element with the specified key (m\\[key]) from the map. If m is nil or there is no such element, delete is a no-op.", + "detail": "func(m map[Type]Type1, key Type)", "insertText": "delete(${1:m}, ${2:key})", - "insertTextRules": 4, - "kind": 1 + "signature": "func delete(m map[Type]Type1, key Type)", + "kind": 1, + "insertTextRules": 4 }, { "label": "len", - "documentation": { - "value": "The len built-in function returns the length of v, according to its type:\n\n\tArray: the number of elements in v.\n\tPointer to array: the number of elements in *v (even if v is nil).\n\tSlice, or map: the number of elements in v; if v is nil, len(v) is zero.\n\tString: the number of bytes in v.\n\tChannel: the number of elements queued (unread) in the channel buffer;\n\t if v is nil, len(v) is zero.\n\nFor some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification's \"Length and capacity\" section for details." - }, - "detail": "func len(v Type) int", + "documentation": "The len built-in function returns the length of v, according to its type:\n\n\tArray: the number of elements in v.\n\tPointer to array: the number of elements in *v (even if v is nil).\n\tSlice, or map: the number of elements in v; if v is nil, len(v) is zero.\n\tString: the number of bytes in v.\n\tChannel: the number of elements queued (unread) in the channel buffer;\n\t if v is nil, len(v) is zero.\n\nFor some arguments, such as a string literal or a simple array expression, the result can be a constant. See the Go language specification's \"Length and capacity\" section for details.", + "detail": "func(v Type) int", "insertText": "len(${1:v})", - "insertTextRules": 4, - "kind": 1 + "signature": "func len(v Type) int", + "kind": 1, + "insertTextRules": 4 }, { "label": "cap", - "documentation": { - "value": "The cap built-in function returns the capacity of v, according to its type:\n\n\tArray: the number of elements in v (same as len(v)).\n\tPointer to array: the number of elements in *v (same as len(v)).\n\tSlice: the maximum length the slice can reach when resliced;\n\tif v is nil, cap(v) is zero.\n\tChannel: the channel buffer capacity, in units of elements;\n\tif v is nil, cap(v) is zero.\n\nFor some arguments, such as a simple array expression, the result can be a constant. See the Go language specification's \"Length and capacity\" section for details." - }, - "detail": "func cap(v Type) int", + "documentation": "The cap built-in function returns the capacity of v, according to its type:\n\n\tArray: the number of elements in v (same as len(v)).\n\tPointer to array: the number of elements in *v (same as len(v)).\n\tSlice: the maximum length the slice can reach when resliced;\n\tif v is nil, cap(v) is zero.\n\tChannel: the channel buffer capacity, in units of elements;\n\tif v is nil, cap(v) is zero.\n\nFor some arguments, such as a simple array expression, the result can be a constant. See the Go language specification's \"Length and capacity\" section for details.", + "detail": "func(v Type) int", "insertText": "cap(${1:v})", - "insertTextRules": 4, - "kind": 1 + "signature": "func cap(v Type) int", + "kind": 1, + "insertTextRules": 4 }, { "label": "make", - "documentation": { - "value": "The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make's return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type:\n\n\tSlice: The size specifies the length. The capacity of the slice is\n\tequal to its length. A second integer argument may be provided to\n\tspecify a different capacity; it must be no smaller than the\n\tlength. For example, make([]int, 0, 10) allocates an underlying array\n\tof size 10 and returns a slice of length 0 and capacity 10 that is\n\tbacked by this underlying array.\n\tMap: An empty map is allocated with enough space to hold the\n\tspecified number of elements. The size may be omitted, in which case\n\ta small starting size is allocated.\n\tChannel: The channel's buffer is initialized with the specified\n\tbuffer capacity. If zero, or the size is omitted, the channel is\n\tunbuffered." - }, - "detail": "func make(t Type, size ...IntegerType) Type", + "documentation": "The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make's return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type:\n\n\tSlice: The size specifies the length. The capacity of the slice is\n\tequal to its length. A second integer argument may be provided to\n\tspecify a different capacity; it must be no smaller than the\n\tlength. For example, make([]int, 0, 10) allocates an underlying array\n\tof size 10 and returns a slice of length 0 and capacity 10 that is\n\tbacked by this underlying array.\n\tMap: An empty map is allocated with enough space to hold the\n\tspecified number of elements. The size may be omitted, in which case\n\ta small starting size is allocated.\n\tChannel: The channel's buffer is initialized with the specified\n\tbuffer capacity. If zero, or the size is omitted, the channel is\n\tunbuffered.", + "detail": "func(t Type, size ...IntegerType) Type", "insertText": "make(${1:t}, ${2:size})", - "insertTextRules": 4, - "kind": 1 + "signature": "func make(t Type, size ...IntegerType) Type", + "kind": 1, + "insertTextRules": 4 }, { "label": "max", - "documentation": { - "value": "The max built-in function returns the largest value of a fixed number of arguments of [cmp.Ordered](/cmp#Ordered) types. There must be at least one argument. If T is a floating-point type and any of the arguments are NaNs, max will return NaN." - }, - "detail": "func max[T cmp.Ordered](x T, y ...T) T", + "documentation": "The max built-in function returns the largest value of a fixed number of arguments of [cmp.Ordered](/cmp#Ordered) types. There must be at least one argument. If T is a floating-point type and any of the arguments are NaNs, max will return NaN.", + "detail": "func[T cmp.Ordered](x T, y ...T) T", "insertText": "max[${1:T}](${2:x}, ${3:y})", - "insertTextRules": 4, - "kind": 1 + "signature": "func max[T cmp.Ordered](x T, y ...T) T", + "kind": 1, + "insertTextRules": 4 }, { "label": "min", - "documentation": { - "value": "The min built-in function returns the smallest value of a fixed number of arguments of [cmp.Ordered](/cmp#Ordered) types. There must be at least one argument. If T is a floating-point type and any of the arguments are NaNs, min will return NaN." - }, - "detail": "func min[T cmp.Ordered](x T, y ...T) T", + "documentation": "The min built-in function returns the smallest value of a fixed number of arguments of [cmp.Ordered](/cmp#Ordered) types. There must be at least one argument. If T is a floating-point type and any of the arguments are NaNs, min will return NaN.", + "detail": "func[T cmp.Ordered](x T, y ...T) T", "insertText": "min[${1:T}](${2:x}, ${3:y})", - "insertTextRules": 4, - "kind": 1 + "signature": "func min[T cmp.Ordered](x T, y ...T) T", + "kind": 1, + "insertTextRules": 4 }, { "label": "new", - "documentation": { - "value": "The new built-in function allocates memory. The first argument is a type, not a value, and the value returned is a pointer to a newly allocated zero value of that type." - }, - "detail": "func new(Type) *Type", + "documentation": "The new built-in function allocates memory. The first argument is a type, not a value, and the value returned is a pointer to a newly allocated zero value of that type.", + "detail": "func(Type) *Type", "insertText": "new()", - "insertTextRules": 4, - "kind": 1 + "signature": "func new(Type) *Type", + "kind": 1, + "insertTextRules": 4 }, { "label": "complex", - "documentation": { - "value": "The complex built-in function constructs a complex value from two floating-point values. The real and imaginary parts must be of the same size, either float32 or float64 (or assignable to them), and the return value will be the corresponding complex type (complex64 for float32, complex128 for float64)." - }, - "detail": "func complex(r, i FloatType) ComplexType", + "documentation": "The complex built-in function constructs a complex value from two floating-point values. The real and imaginary parts must be of the same size, either float32 or float64 (or assignable to them), and the return value will be the corresponding complex type (complex64 for float32, complex128 for float64).", + "detail": "func(r, i FloatType) ComplexType", "insertText": "complex(${1:r}, ${2:i})", - "insertTextRules": 4, - "kind": 1 + "signature": "func complex(r, i FloatType) ComplexType", + "kind": 1, + "insertTextRules": 4 }, { "label": "real", - "documentation": { - "value": "The real built-in function returns the real part of the complex number c. The return value will be floating point type corresponding to the type of c." - }, - "detail": "func real(c ComplexType) FloatType", + "documentation": "The real built-in function returns the real part of the complex number c. The return value will be floating point type corresponding to the type of c.", + "detail": "func(c ComplexType) FloatType", "insertText": "real(${1:c})", - "insertTextRules": 4, - "kind": 1 + "signature": "func real(c ComplexType) FloatType", + "kind": 1, + "insertTextRules": 4 }, { "label": "imag", - "documentation": { - "value": "The imag built-in function returns the imaginary part of the complex number c. The return value will be floating point type corresponding to the type of c." - }, - "detail": "func imag(c ComplexType) FloatType", + "documentation": "The imag built-in function returns the imaginary part of the complex number c. The return value will be floating point type corresponding to the type of c.", + "detail": "func(c ComplexType) FloatType", "insertText": "imag(${1:c})", - "insertTextRules": 4, - "kind": 1 + "signature": "func imag(c ComplexType) FloatType", + "kind": 1, + "insertTextRules": 4 }, { "label": "clear", - "documentation": { - "value": "The clear built-in function clears maps and slices. For maps, clear deletes all entries, resulting in an empty map. For slices, clear sets all elements up to the length of the slice to the zero value of the respective element type. If the argument type is a type parameter, the type parameter's type set must contain only map or slice types, and clear performs the operation implied by the type argument." - }, - "detail": "func clear[T ~[]Type | ~map[Type]Type1](t T)", + "documentation": "The clear built-in function clears maps and slices. For maps, clear deletes all entries, resulting in an empty map. For slices, clear sets all elements up to the length of the slice to the zero value of the respective element type. If the argument type is a type parameter, the type parameter's type set must contain only map or slice types, and clear performs the operation implied by the type argument.", + "detail": "func[T ~[]Type | ~map[Type]Type1](t T)", "insertText": "clear[${1:T}](${2:t})", - "insertTextRules": 4, - "kind": 1 + "signature": "func clear[T ~[]Type | ~map[Type]Type1](t T)", + "kind": 1, + "insertTextRules": 4 }, { "label": "close", - "documentation": { - "value": "The close built-in function closes a channel, which must be either bidirectional or send-only. It should be executed only by the sender, never the receiver, and has the effect of shutting down the channel after the last sent value is received. After the last value has been received from a closed channel c, any receive from c will succeed without blocking, returning the zero value for the channel element. The form\n\n\tx, ok := \u003c-c\n\nwill also set ok to false for a closed and empty channel." - }, - "detail": "func close(c chan\u003c- Type)", + "documentation": "The close built-in function closes a channel, which must be either bidirectional or send-only. It should be executed only by the sender, never the receiver, and has the effect of shutting down the channel after the last sent value is received. After the last value has been received from a closed channel c, any receive from c will succeed without blocking, returning the zero value for the channel element. The form\n\n\tx, ok := \u003c-c\n\nwill also set ok to false for a closed and empty channel.", + "detail": "func(c chan\u003c- Type)", "insertText": "close(${1:c})", - "insertTextRules": 4, - "kind": 1 + "signature": "func close(c chan\u003c- Type)", + "kind": 1, + "insertTextRules": 4 }, { "label": "panic", - "documentation": { - "value": "The panic built-in function stops normal execution of the current goroutine. When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic, terminating G's execution and running any deferred functions. This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated with a non-zero exit code. This termination sequence is called panicking and can be controlled by the built-in function recover.\n\nStarting in Go 1.21, calling panic with a nil interface value or an untyped nil causes a run-time error (a different panic). The GODEBUG setting panicnil=1 disables the run-time error." - }, - "detail": "func panic(v any)", + "documentation": "The panic built-in function stops normal execution of the current goroutine. When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic, terminating G's execution and running any deferred functions. This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated with a non-zero exit code. This termination sequence is called panicking and can be controlled by the built-in function recover.\n\nStarting in Go 1.21, calling panic with a nil interface value or an untyped nil causes a run-time error (a different panic). The GODEBUG setting panicnil=1 disables the run-time error.", + "detail": "func(v any)", "insertText": "panic(${1:v})", - "insertTextRules": 4, - "kind": 1 + "signature": "func panic(v any)", + "kind": 1, + "insertTextRules": 4 }, { "label": "recover", - "documentation": { - "value": "The recover built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, recover returns nil.\n\nPrior to Go 1.21, recover would also return nil if panic is called with a nil argument. See \\[panic] for details." - }, - "detail": "func recover() any", + "documentation": "The recover built-in function allows a program to manage behavior of a panicking goroutine. Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, recover returns nil.\n\nPrior to Go 1.21, recover would also return nil if panic is called with a nil argument. See \\[panic] for details.", + "detail": "func() any", "insertText": "recover()", - "insertTextRules": 4, - "kind": 1 + "signature": "func recover() any", + "kind": 1, + "insertTextRules": 4 }, { "label": "print", - "documentation": { - "value": "The print built-in function formats its arguments in an implementation-specific way and writes the result to standard error. Print is useful for bootstrapping and debugging; it is not guaranteed to stay in the language." - }, - "detail": "func print(args ...Type)", + "documentation": "The print built-in function formats its arguments in an implementation-specific way and writes the result to standard error. Print is useful for bootstrapping and debugging; it is not guaranteed to stay in the language.", + "detail": "func(args ...Type)", "insertText": "print(${1:args})", - "insertTextRules": 4, - "kind": 1 + "signature": "func print(args ...Type)", + "kind": 1, + "insertTextRules": 4 }, { "label": "println", - "documentation": { - "value": "The println built-in function formats its arguments in an implementation-specific way and writes the result to standard error. Spaces are always added between arguments and a newline is appended. Println is useful for bootstrapping and debugging; it is not guaranteed to stay in the language." - }, - "detail": "func println(args ...Type)", + "documentation": "The println built-in function formats its arguments in an implementation-specific way and writes the result to standard error. Spaces are always added between arguments and a newline is appended. Println is useful for bootstrapping and debugging; it is not guaranteed to stay in the language.", + "detail": "func(args ...Type)", "insertText": "println(${1:args})", - "insertTextRules": 4, - "kind": 1 + "signature": "func println(args ...Type)", + "kind": 1, + "insertTextRules": 4 }, { "label": "error", - "documentation": { - "value": "The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error." - }, - "detail": "type error interface {\n\tError() string\n}", + "documentation": "The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error.", + "detail": "interface{...}", "insertText": "error", - "kind": 7 + "signature": "type error interface {\n\tError() string\n}", + "kind": 7, + "insertTextRules": 0 } ] diff --git a/internal/pkgindex/docutil/testdata/simple/expect.json b/internal/pkgindex/docutil/testdata/simple/expect.json index 0120c31c..548b46aa 100644 --- a/internal/pkgindex/docutil/testdata/simple/expect.json +++ b/internal/pkgindex/docutil/testdata/simple/expect.json @@ -1,33 +1,39 @@ [ { "label": "ErrInvalidUnreadByte", - "documentation": { - "value": "ErrInvalidUnreadByte occurs when invalid use of UnreadByte is done." - }, + "documentation": "ErrInvalidUnreadByte occurs when invalid use of UnreadByte is done.", "insertText": "ErrInvalidUnreadByte", - "kind": 4 + "kind": 4, + "insertTextRules": 0 }, { "label": "Bar", - "documentation": null, "detail": "var Bar = 32", "insertText": "Bar", - "kind": 4 + "kind": 4, + "insertTextRules": 0 }, { "label": "Foo", - "documentation": null, "detail": "var Foo int32", "insertText": "Foo", - "kind": 4 + "kind": 4, + "insertTextRules": 0 }, { "label": "Baz", - "documentation": { - "value": "Baz is a sample value." - }, + "documentation": "Baz is a sample value.", "detail": "var Baz bool", "insertText": "Baz", - "kind": 4 + "kind": 4, + "insertTextRules": 0 + }, + { + "label": "MyPublicFunc", + "detail": "func(foo int) (int, error)", + "insertText": "MyPublicFunc(${1:foo})", + "signature": "func MyPublicFunc(foo int) (int, error)", + "kind": 1, + "insertTextRules": 4 } ] diff --git a/internal/pkgindex/docutil/testdata/simple/types.go b/internal/pkgindex/docutil/testdata/simple/types.go index 30270d1c..c1dcb40f 100644 --- a/internal/pkgindex/docutil/testdata/simple/types.go +++ b/internal/pkgindex/docutil/testdata/simple/types.go @@ -11,4 +11,11 @@ var ( // Baz is a sample value. Baz bool = false + + // this var should be ignored + private = true ) + +func MyPublicFunc(foo int) (int, error) { + return foo / 32, nil +} diff --git a/internal/pkgindex/docutil/traverse.go b/internal/pkgindex/docutil/traverse.go index 1cd0ad42..e4fbbdc7 100644 --- a/internal/pkgindex/docutil/traverse.go +++ b/internal/pkgindex/docutil/traverse.go @@ -15,10 +15,12 @@ type TraverseOpts struct { SnippetFormat monaco.CompletionItemInsertTextRule } -type TraverseReducer = func(items ...monaco.CompletionItem) +type TraverseReducer = func(items ...Symbol) -// CollectCompletionItems traverses root file declarations and transforms them into completion items. -func CollectCompletionItems(decls []ast.Decl, opts TraverseOpts, reducer TraverseReducer) error { +// CollectSymbols traverses root file declarations and transforms them into completion items. +// +// Important: type methods are ignored. +func CollectSymbols(decls []ast.Decl, opts TraverseOpts, reducer TraverseReducer) error { filter := filterOrDefault(opts.Filter) for _, decl := range decls { switch t := decl.(type) { @@ -27,7 +29,12 @@ func CollectCompletionItems(decls []ast.Decl, opts TraverseOpts, reducer Travers continue } - item, err := CompletionItemFromFunc(opts.FileSet, t, monaco.InsertAsSnippet) + if t.Recv != nil { + // Ignore type methods, at-least for now. + continue + } + + item, err := SymbolFromFunc(opts.FileSet, t, monaco.InsertAsSnippet) if err != nil { return fmt.Errorf( "can't parse function %s: %w (pos: %s)", @@ -41,7 +48,7 @@ func CollectCompletionItems(decls []ast.Decl, opts TraverseOpts, reducer Travers continue } - items, err := DeclToCompletionItem(opts.FileSet, t, filter) + items, err := DeclToSymbol(opts.FileSet, t, filter) if err != nil { return fmt.Errorf( "can't parse decl %s: %w (at %s)", diff --git a/internal/pkgindex/docutil/traverse_test.go b/internal/pkgindex/docutil/traverse_test.go index f63aa49d..1412128d 100644 --- a/internal/pkgindex/docutil/traverse_test.go +++ b/internal/pkgindex/docutil/traverse_test.go @@ -6,7 +6,6 @@ import ( "go/token" "golang.org/x/exp/slices" "os" - "strings" "testing" "github.com/stretchr/testify/require" @@ -21,11 +20,11 @@ func TestTypeToCompletionItem(t *testing.T) { dumpOnly bool ignore []string }{ - //"bufio": { - // filePath: "testdata/bufio/bufio.go", - //}, + "bufio": { + filePath: "testdata/bufio/bufio.go", + expectFile: "testdata/bufio/expect.json", + }, "simple": { - dumpOnly: true, filePath: "testdata/simple/types.go", expectFile: "testdata/simple/expect.json", }, @@ -54,10 +53,10 @@ func TestTypeToCompletionItem(t *testing.T) { } var ( - got []monaco.CompletionItem - want []monaco.CompletionItem + got []Symbol + want []Symbol ) - err = CollectCompletionItems(r.Decls, opts, func(items ...monaco.CompletionItem) { + err = CollectSymbols(r.Decls, opts, func(items ...Symbol) { got = append(got, items...) }) if c.expectErr != "" { @@ -81,11 +80,11 @@ func TestTypeToCompletionItem(t *testing.T) { }) require.NoError(t, json.NewDecoder(f).Decode(&want)) - slices.SortFunc(got, func(a, b monaco.CompletionItem) bool { - return strings.Compare(a.Label.String, b.Label.String) == -1 + slices.SortFunc(got, func(a, b Symbol) bool { + return a.Compare(b) == -1 }) - slices.SortFunc(want, func(a, b monaco.CompletionItem) bool { - return strings.Compare(a.Label.String, b.Label.String) == -1 + slices.SortFunc(want, func(a, b Symbol) bool { + return a.Compare(b) == -1 }) require.Equal(t, want, got) @@ -93,7 +92,7 @@ func TestTypeToCompletionItem(t *testing.T) { } } -func dumpCompletionItems(t *testing.T, items []monaco.CompletionItem, dst string) { +func dumpCompletionItems(t *testing.T, items []Symbol, dst string) { f, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) require.NoError(t, err) defer f.Close() diff --git a/internal/pkgindex/docutil/type.go b/internal/pkgindex/docutil/type.go index 812d9d0b..037f0c18 100644 --- a/internal/pkgindex/docutil/type.go +++ b/internal/pkgindex/docutil/type.go @@ -41,23 +41,24 @@ func NewBlockData(specGroup *ast.GenDecl) (BlockData, error) { }, nil } -// TypeToCompletionItem returns completion item from type declaration inside block. -func TypeToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.TypeSpec) (monaco.CompletionItem, error) { +// TypeToSymbol returns completion item from type declaration inside block. +func TypeToSymbol(fset *token.FileSet, block BlockData, spec *ast.TypeSpec) (Symbol, error) { // Block declarations contain doc inside each child. - doc := getTypeDoc(block, spec) - - item := monaco.CompletionItem{ - Kind: block.Kind, - InsertText: spec.Name.Name, + item := Symbol{ + Label: spec.Name.Name, + Kind: block.Kind, + InsertText: spec.Name.Name, + Documentation: getTypeDoc(block, spec), } - item.Label.String = spec.Name.Name isPrimitive := false switch spec.Type.(type) { case *ast.InterfaceType: + item.Detail = "interface{...}" item.Kind = monaco.Interface case *ast.StructType: // TODO: prefill struct members + item.Detail = "struct{...}" item.InsertText = item.InsertText + "{}" item.Kind = monaco.Struct case *ast.Ident: @@ -67,17 +68,16 @@ func TypeToCompletionItem(fset *token.FileSet, block BlockData, spec *ast.TypeSp if !isPrimitive { signature, err := PrintDecl(fset, block.Decl) if err != nil { - return item, fmt.Errorf("%w (type: %q, pos: %s)", err, item.Label.String, GetDeclPosition(fset, spec)) + return item, fmt.Errorf("%w (type: %q, pos: %s)", err, item.Label, GetDeclPosition(fset, spec)) } - item.Detail = signature + item.Signature = signature } - item.Documentation.SetValue(doc) return item, nil } -func getTypeDoc(block BlockData, spec *ast.TypeSpec) *monaco.IMarkdownString { +func getTypeDoc(block BlockData, spec *ast.TypeSpec) string { g := block.Decl.Doc if block.IsGroup || len(block.Decl.Specs) > 1 { // standalone type declarations are still considered as block. @@ -85,10 +85,8 @@ func getTypeDoc(block BlockData, spec *ast.TypeSpec) *monaco.IMarkdownString { } if CommentGroupEmpty(g) { - return nil + return "" } - return &monaco.IMarkdownString{ - Value: string(FormatCommentGroup(g)), - } + return string(FormatCommentGroup(g)) } diff --git a/internal/pkgindex/docutil/types.go b/internal/pkgindex/docutil/types.go new file mode 100644 index 00000000..249eb90f --- /dev/null +++ b/internal/pkgindex/docutil/types.go @@ -0,0 +1,25 @@ +package docutil + +import ( + "strings" + + "github.com/x1unix/go-playground/pkg/monaco" +) + +// Symbol is a generic primitive that holds symbol summary. +// +// Type is a type similar to monaco's CompletionItem but contains additional type data. +type Symbol struct { + Label string `json:"label"` + Documentation string `json:"documentation,omitempty"` + Detail string `json:"detail,omitempty"` + InsertText string `json:"insertText"` + Signature string `json:"signature,omitempty"` + Kind monaco.CompletionItemKind `json:"kind"` + InsertTextRules monaco.CompletionItemInsertTextRule `json:"insertTextRules"` +} + +// Compare compares two symbol for sorting. +func (sym Symbol) Compare(b Symbol) int { + return strings.Compare(sym.Label, b.Label) +} diff --git a/internal/pkgindex/docutil/utils.go b/internal/pkgindex/docutil/utils.go index 27c98c1f..c18467ec 100644 --- a/internal/pkgindex/docutil/utils.go +++ b/internal/pkgindex/docutil/utils.go @@ -114,8 +114,8 @@ func WriteParamsList(sb *strings.Builder, snippetIndex int, params *ast.FieldLis return offset } -// DeclToString returns string representation of passed AST node. -func DeclToString(fset *token.FileSet, decl any) (string, error) { +// PrintDecl returns string representation of passed AST node. +func PrintDecl(fset *token.FileSet, decl any) (string, error) { // Remove comments block from AST node to keep only node body trimmedDecl := removeCommentFromDecl(decl) diff --git a/internal/pkgindex/docutil/value.go b/internal/pkgindex/docutil/value.go index f3663c18..4a344bf7 100644 --- a/internal/pkgindex/docutil/value.go +++ b/internal/pkgindex/docutil/value.go @@ -4,8 +4,6 @@ import ( "fmt" "go/ast" "go/token" - - "github.com/x1unix/go-playground/pkg/monaco" ) type TraverseContext struct { @@ -14,14 +12,14 @@ type TraverseContext struct { Filter Filter } -// ValueToCompletionItem constructs completion item from value declaration. +// ValueToSymbol constructs completion item from value declaration. // // Able to handle special edge cases for builtin declarations. -func ValueToCompletionItem(ctx TraverseContext, spec *ast.ValueSpec) ([]monaco.CompletionItem, error) { +func ValueToSymbol(ctx TraverseContext, spec *ast.ValueSpec) ([]Symbol, error) { filter := filterOrDefault(ctx.Filter) blockDoc := getValueDocumentation(ctx.Block, spec) - items := make([]monaco.CompletionItem, 0, len(spec.Values)) + items := make([]Symbol, 0, len(spec.Values)) for _, val := range spec.Names { if filter.Ignore(val.Name) { continue @@ -32,33 +30,31 @@ func ValueToCompletionItem(ctx TraverseContext, spec *ast.ValueSpec) ([]monaco.C return nil, err } - item := monaco.CompletionItem{ - Kind: ctx.Block.Kind, - InsertText: val.Name, - Detail: detail, + item := Symbol{ + Label: val.Name, + Kind: ctx.Block.Kind, + InsertText: val.Name, + Detail: detail, + Documentation: blockDoc, } - item.Label.String = val.Name - item.Documentation.SetValue(blockDoc) items = append(items, item) } return items, nil } -func getValueDocumentation(block BlockData, spec *ast.ValueSpec) *monaco.IMarkdownString { +func getValueDocumentation(block BlockData, spec *ast.ValueSpec) string { g := block.Decl.Doc if block.IsGroup || len(block.Decl.Specs) > 1 || CommentGroupEmpty(g) { g = spec.Doc } if CommentGroupEmpty(g) { - return nil + return "" } - return &monaco.IMarkdownString{ - Value: string(FormatCommentGroup(g)), - } + return string(FormatCommentGroup(g)) } func detailFromIdent(fset *token.FileSet, block BlockData, ident *ast.Ident) (string, error) { diff --git a/internal/pkgindex/index/parse.go b/internal/pkgindex/index/parse.go index 1548eecd..42f0d699 100644 --- a/internal/pkgindex/index/parse.go +++ b/internal/pkgindex/index/parse.go @@ -59,9 +59,9 @@ func parseFile(fset *token.FileSet, fpath string, params fileParseParams) (*sour SnippetFormat: monaco.InsertAsSnippet, } - err = docutil.CollectCompletionItems(root.Decls, opts, func(items ...monaco.CompletionItem) { + err = docutil.CollectSymbols(root.Decls, opts, func(items ...docutil.Symbol) { for _, item := range items { - summary.symbols = append(summary.symbols, SymbolInfoFromCompletionItem(item, src)) + summary.symbols = append(summary.symbols, IntoSymbolInfo(item, src)) } }) diff --git a/internal/pkgindex/index/types.go b/internal/pkgindex/index/types.go index 0e05fcf1..05b1731b 100644 --- a/internal/pkgindex/index/types.go +++ b/internal/pkgindex/index/types.go @@ -1,6 +1,9 @@ package index -import "github.com/x1unix/go-playground/pkg/monaco" +import ( + "github.com/x1unix/go-playground/internal/pkgindex/docutil" + "github.com/x1unix/go-playground/pkg/monaco" +) const GoIndexFileVersion = 1 @@ -25,6 +28,9 @@ type SymbolInfo struct { // Detail is symbol summary. Detail string `json:"detail,omitempty"` + // Signature contains type declaration including public fields. + Signature string `json:"signature,omitempty"` + // InsertText is text to be inserted by completion. InsertText string `json:"insertText"` @@ -38,24 +44,15 @@ type SymbolInfo struct { Package SymbolSource `json:"package"` } -func SymbolInfoFromCompletionItem(item monaco.CompletionItem, src SymbolSource) SymbolInfo { - doc := item.Documentation.String - if item.Documentation.Value != nil { - doc = item.Documentation.Value.Value - } - - label := item.Label.String - if item.Label.Value != nil { - label = item.Label.Value.Label - } - +func IntoSymbolInfo(item docutil.Symbol, src SymbolSource) SymbolInfo { return SymbolInfo{ - Name: label, - Doc: doc, + Name: item.Label, + Doc: item.Documentation, Detail: item.Detail, InsertText: item.InsertText, InsertTextRules: item.InsertTextRules, Kind: item.Kind, + Signature: item.Signature, Package: src, } } From 10a8f54ecb965b34d1c90296a980b50fc71faae2 Mon Sep 17 00:00:00 2001 From: x1unix Date: Tue, 1 Oct 2024 01:41:02 -0400 Subject: [PATCH 16/43] fix: fix IsGoSourceFile --- internal/pkgindex/docutil/utils.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkgindex/docutil/utils.go b/internal/pkgindex/docutil/utils.go index c18467ec..0cdc4032 100644 --- a/internal/pkgindex/docutil/utils.go +++ b/internal/pkgindex/docutil/utils.go @@ -37,7 +37,7 @@ func TokenToCompletionItemKind(tok token.Token) (monaco.CompletionItemKind, bool // // Returns false for unit test files (this is intentional behavior). func IsGoSourceFile(name string) bool { - return strings.HasSuffix(name, "_test.go") || !strings.HasSuffix(name, ".go") + return strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go") } // CommentGroupEmpty checks whether passed command group is empty. From 405e201f687a3b8ea92b2c69b882b4f529aee6ed Mon Sep 17 00:00:00 2001 From: x1unix Date: Tue, 1 Oct 2024 02:07:06 -0400 Subject: [PATCH 17/43] feat: use predefined buff sizes --- internal/pkgindex/index/scanner.go | 19 ++++++++++++------- tools/pkgindexer/main.go | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/internal/pkgindex/index/scanner.go b/internal/pkgindex/index/scanner.go index 94c51813..37d39f22 100644 --- a/internal/pkgindex/index/scanner.go +++ b/internal/pkgindex/index/scanner.go @@ -13,10 +13,16 @@ import ( "github.com/x1unix/go-playground/internal/pkgindex/imports" ) -// queueSize is based on max occupation of a queue during test scan of Go 1.23. -// -// See: Queue.MaxOccupancy -const queueSize = 120 +const ( + // queueSize is based on max occupation of a queue during test scan of Go 1.23. + // + // See: Queue.MaxOccupancy + queueSize = 120 + + // Go 1.23 has 185 packages and over 70k total symbols. + pkgBuffSize = 185 + symBuffSize = 78000 +) type scanEntry struct { isVendor bool @@ -45,9 +51,8 @@ func ScanRoot(goRoot string) (*GoIndexFile, error) { return nil, err } - // There are 213 packages in Go 1.23 - packages := make([]PackageInfo, 0, 213) - symbols := make([]SymbolInfo, 0, 300) + packages := make([]PackageInfo, 0, pkgBuffSize) + symbols := make([]SymbolInfo, 0, symBuffSize) for queue.Occupied() { v, ok := queue.Pop() diff --git a/tools/pkgindexer/main.go b/tools/pkgindexer/main.go index 68648d11..b96443bc 100644 --- a/tools/pkgindexer/main.go +++ b/tools/pkgindexer/main.go @@ -109,7 +109,7 @@ func runGenIndex(flags Flags) error { return err } - log.Printf("Scanned %d packages", len(entries.Packages)) + log.Printf("Scanned %d packages and %d symbols", len(entries.Packages), len(entries.Symbols)) return nil } From 37762d102ebf1703f0618bc3c50cbefdff3891c1 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 00:52:03 -0400 Subject: [PATCH 18/43] feat: restructure pkgindex --- internal/pkgindex/cmd/common.go | 46 +++++++++ internal/pkgindex/cmd/flags.go | 85 +++++++++++++++++ internal/pkgindex/cmd/imports.go | 45 +++++++++ internal/pkgindex/cmd/index.go | 48 ++++++++++ internal/pkgindex/cmd/root.go | 32 +++++++ tools/pkgindexer/main.go | 157 +------------------------------ 6 files changed, 258 insertions(+), 155 deletions(-) create mode 100644 internal/pkgindex/cmd/common.go create mode 100644 internal/pkgindex/cmd/flags.go create mode 100644 internal/pkgindex/cmd/imports.go create mode 100644 internal/pkgindex/cmd/index.go create mode 100644 internal/pkgindex/cmd/root.go diff --git a/internal/pkgindex/cmd/common.go b/internal/pkgindex/cmd/common.go new file mode 100644 index 00000000..1f1cda2a --- /dev/null +++ b/internal/pkgindex/cmd/common.go @@ -0,0 +1,46 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" +) + +func writeOutput(flags importsFlags, data any) error { + if flags.outFile == "" { + return getEncoder(os.Stdout, true).Encode(data) + } + + if err := os.MkdirAll(filepath.Dir(flags.outFile), 0755); err != nil { + return fmt.Errorf("failed to pre-create parent directories: %w", err) + } + + f, err := os.OpenFile(flags.outFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + defer silentClose(f) + + if err != nil { + return fmt.Errorf("can't create output file: %w", err) + } + + if err := getEncoder(f, flags.prettyPrint).Encode(data); err != nil { + return fmt.Errorf("can't write JSON to file %q: %w", flags.outFile, err) + } + + return nil +} + +func getEncoder(dst io.Writer, pretty bool) *json.Encoder { + enc := json.NewEncoder(dst) + if pretty { + enc.SetIndent("", " ") + } + + return enc +} + +func silentClose(c io.Closer) { + // I don't care + _ = c.Close() +} diff --git a/internal/pkgindex/cmd/flags.go b/internal/pkgindex/cmd/flags.go new file mode 100644 index 00000000..ab2c8b8a --- /dev/null +++ b/internal/pkgindex/cmd/flags.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "fmt" + + "github.com/x1unix/go-playground/internal/pkgindex/imports" +) + +const ( + formatJSON = "json" + formatProto = "proto" +) + +type globalFlags struct { + goRoot string +} + +func (f globalFlags) withDefaults() (globalFlags, error) { + if f.goRoot != "" { + return f, nil + } + + goRoot, err := imports.ResolveGoRoot() + if err != nil { + return f, fmt.Errorf( + "cannot find GOROOT, please set GOROOT path or check if Go is installed.\nError: %w", + err, + ) + } + f.goRoot = goRoot + return f, err +} + +type importsFlags struct { + *globalFlags + prettyPrint bool + stdout bool + outFile string +} + +func (f importsFlags) validate() error { + if f.outFile == "" && !f.stdout { + return fmt.Errorf("missing output file flag. Use --stdout flag to print into stdout") + } + + if f.stdout && f.outFile != "" { + return fmt.Errorf("ambiguous output flag: --stdout and output file flag can't be together") + } + + return nil +} + +type indexFlags struct { + importsFlags + + format string +} + +func (f indexFlags) validate() error { + switch f.format { + case "proto": + if f.stdout { + return fmt.Errorf("--stdout flag not allowed for Protobuf format") + } + if f.prettyPrint { + return fmt.Errorf("pretty print is not avaiable for Protobuf format") + } + if f.outFile == "" { + return fmt.Errorf("missing output file flag") + } + return nil + case "", "json": + if f.outFile == "" && !f.stdout { + return fmt.Errorf("missing output file flag. Use --stdout flag to print into stdout") + } + + if f.stdout && f.outFile != "" { + return fmt.Errorf("ambiguous output flag: --stdout and output file flag can't be together") + } + default: + return fmt.Errorf("unsupported output format %q", f.format) + } + + return nil +} diff --git a/internal/pkgindex/cmd/imports.go b/internal/pkgindex/cmd/imports.go new file mode 100644 index 00000000..f22208ea --- /dev/null +++ b/internal/pkgindex/cmd/imports.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "log" + + "github.com/spf13/cobra" + "github.com/x1unix/go-playground/internal/pkgindex/imports" +) + +func newCmdImports(g *globalFlags) *cobra.Command { + flags := importsFlags{ + globalFlags: g, + } + cmd := &cobra.Command{ + Use: "imports [-r goroot] [-o output]", + Short: "Generate imports.json file for old Playground version", + Long: "Generate imports file which contains list of all importable packages. Used in legacy app versions", + PreRunE: func(_ *cobra.Command, _ []string) error { + return flags.validate() + }, + RunE: func(_ *cobra.Command, _ []string) error { + return runGenImports(flags) + }, + } + + cmd.Flags().StringVarP(&flags.outFile, "output", "o", "", "Path to output file. When enpty, prints to stdout") + cmd.Flags().BoolVarP(&flags.prettyPrint, "pretty", "P", false, "Add indents to JSON output") + cmd.Flags().BoolVar(&flags.stdout, "stdout", false, "Dump result into stdout") + return cmd +} + +func runGenImports(flags importsFlags) error { + scanner := imports.NewGoRootScanner(flags.goRoot) + results, err := scanner.Scan() + if err != nil { + return err + } + + if err := writeOutput(flags, results); err != nil { + return err + } + + log.Printf("Scanned %d packages", len(results.Packages)) + return nil +} diff --git a/internal/pkgindex/cmd/index.go b/internal/pkgindex/cmd/index.go new file mode 100644 index 00000000..d4144331 --- /dev/null +++ b/internal/pkgindex/cmd/index.go @@ -0,0 +1,48 @@ +package cmd + +import ( + "log" + + "github.com/spf13/cobra" + "github.com/x1unix/go-playground/internal/pkgindex/index" +) + +func newCmdIndex(g *globalFlags) *cobra.Command { + flags := indexFlags{ + importsFlags: importsFlags{ + globalFlags: g, + }, + } + + cmd := &cobra.Command{ + Use: "index [-r goroot] [-o output]", + Short: "Generate index file with standard Go packages and symbols", + Long: "Generate a JSON file that contains list of all standard Go packages and its symbols. Used in new version of app", + PreRunE: func(_ *cobra.Command, _ []string) error { + return flags.validate() + }, + RunE: func(_ *cobra.Command, _ []string) error { + return runGenIndex(flags) + }, + } + + cmd.Flags().StringVarP(&flags.format, "format", "F", "json", "Output format: proto or json") + cmd.Flags().StringVarP(&flags.outFile, "output", "o", "", "Path to output file. When enpty, prints to stdout") + cmd.Flags().BoolVarP(&flags.prettyPrint, "pretty", "P", false, "Add indents to JSON output") + cmd.Flags().BoolVar(&flags.stdout, "stdout", false, "Dump result into stdout") + return cmd +} + +func runGenIndex(flags indexFlags) error { + entries, err := index.ScanRoot(flags.goRoot) + if err != nil { + return err + } + + if err := writeOutput(flags.importsFlags, entries); err != nil { + return err + } + + log.Printf("Scanned %d packages and %d symbols", len(entries.Packages), len(entries.Symbols)) + return nil +} diff --git a/internal/pkgindex/cmd/root.go b/internal/pkgindex/cmd/root.go new file mode 100644 index 00000000..47b5d746 --- /dev/null +++ b/internal/pkgindex/cmd/root.go @@ -0,0 +1,32 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func newCmdRoot() *cobra.Command { + f := new(globalFlags) + cmd := &cobra.Command{ + SilenceUsage: true, + Use: "pkgindexer [-r goroot] [-o output]", + Short: "Go standard library packages scanner", + Long: "Tool to generate Go package autocomplete entries for Monaco editor from Go SDK", + PersistentPreRunE: func(c *cobra.Command, args []string) (err error) { + *f, err = f.withDefaults() + return err + }, + } + + cmd.PersistentFlags().StringVarP( + &f.goRoot, "root", "r", "", "Path to GOROOT. Uses $GOROOT by default", + ) + + cmd.AddCommand(newCmdImports(f)) + cmd.AddCommand(newCmdIndex(f)) + return cmd +} + +func Run() error { + cmd := newCmdRoot() + return cmd.Execute() +} diff --git a/tools/pkgindexer/main.go b/tools/pkgindexer/main.go index b96443bc..fbfcf637 100644 --- a/tools/pkgindexer/main.go +++ b/tools/pkgindexer/main.go @@ -1,166 +1,13 @@ package main import ( - "encoding/json" - "fmt" - "github.com/x1unix/go-playground/internal/pkgindex/index" - "io" - "log" "os" - "path/filepath" - "github.com/spf13/cobra" - "github.com/x1unix/go-playground/internal/pkgindex/imports" + "github.com/x1unix/go-playground/internal/pkgindex/cmd" ) -type Flags struct { - goRoot string - outFile string - prettyPrint bool - stdout bool -} - -func (f Flags) Validate() error { - if f.outFile == "" && !f.stdout { - return fmt.Errorf("missing output file flag. Use --stdout flag to print into stdout") - } - - if f.stdout && f.outFile != "" { - return fmt.Errorf("ambiguous output flag: --stdout and output file flag can't be together") - } - - return nil -} - -func (f Flags) WithDefaults() (Flags, error) { - if err := f.Validate(); err != nil { - return f, err - } - - if f.goRoot != "" { - return f, nil - } - - goRoot, err := imports.ResolveGoRoot() - if err != nil { - return f, fmt.Errorf( - "cannot find GOROOT, please set GOROOT path or check if Go is installed.\nError: %w", - err, - ) - } - f.goRoot = goRoot - return f, err -} - func main() { - var flags Flags - cmd := &cobra.Command{ - SilenceUsage: true, - Use: "pkgindexer [-r goroot] [-o output]", - Short: "Go standard library packages scanner", - Long: "Tool to generate Go package autocomplete entries for Monaco editor from Go SDK", - } - - cmd.AddCommand(&cobra.Command{ - Use: "imports [-r goroot] [-o output]", - Short: "Generate imports.json file for old Playground version", - Long: "Generate imports file which contains list of all importable packages. Used in legacy app versions", - RunE: func(_ *cobra.Command, _ []string) error { - resolvedFlags, err := flags.WithDefaults() - if err != nil { - return err - } - - return runGenImports(resolvedFlags) - }, - }) - - cmd.AddCommand(&cobra.Command{ - Use: "index [-r goroot] [-o output]", - Short: "Generate index file with standard Go packages and symbols", - Long: "Generate a JSON file that contains list of all standard Go packages and its symbols. Used in new version of app", - RunE: func(_ *cobra.Command, _ []string) error { - resolvedFlags, err := flags.WithDefaults() - if err != nil { - return err - } - - return runGenIndex(resolvedFlags) - }, - }) - - cmd.PersistentFlags().StringVarP(&flags.goRoot, "root", "r", "", "Path to GOROOT. Uses $GOROOT by default") - cmd.PersistentFlags().StringVarP(&flags.outFile, "output", "o", "", "Path to output file. When enpty, prints to stdout") - cmd.PersistentFlags().BoolVarP(&flags.prettyPrint, "pretty", "P", false, "Add indents to JSON output") - cmd.PersistentFlags().BoolVar(&flags.stdout, "stdout", false, "Dump result into stdout") - - if err := cmd.Execute(); err != nil { + if err := cmd.Run(); err != nil { os.Exit(2) } } - -func runGenIndex(flags Flags) error { - entries, err := index.ScanRoot(flags.goRoot) - if err != nil { - return err - } - - if err := writeOutput(flags, entries); err != nil { - return err - } - - log.Printf("Scanned %d packages and %d symbols", len(entries.Packages), len(entries.Symbols)) - return nil -} - -func runGenImports(flags Flags) error { - scanner := imports.NewGoRootScanner(flags.goRoot) - results, err := scanner.Scan() - if err != nil { - return err - } - - if err := writeOutput(flags, results); err != nil { - return err - } - - log.Printf("Scanned %d packages", len(results.Packages)) - return nil -} - -func getEncoder(dst io.Writer, pretty bool) *json.Encoder { - enc := json.NewEncoder(dst) - if pretty { - enc.SetIndent("", " ") - } - - return enc -} - -func writeOutput(flags Flags, data any) error { - if flags.outFile == "" { - return getEncoder(os.Stdout, true).Encode(data) - } - - if err := os.MkdirAll(filepath.Dir(flags.outFile), 0755); err != nil { - return fmt.Errorf("failed to pre-create parent directories: %w", err) - } - - f, err := os.OpenFile(flags.outFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) - defer silentClose(f) - - if err != nil { - return fmt.Errorf("can't create output file: %w", err) - } - - if err := getEncoder(f, flags.prettyPrint).Encode(data); err != nil { - return fmt.Errorf("can't write JSON to file %q: %w", flags.outFile, err) - } - - return nil -} - -func silentClose(c io.Closer) { - // I don't care - _ = c.Close() -} From e2c1b8c2ef92c29cb86acb497808a1819db61ea5 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 02:31:07 -0400 Subject: [PATCH 19/43] chore: require protobuf for future developments --- HACKING.md | 3 +++ build.mk | 2 +- go.mod | 4 +++- go.sum | 7 +++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/HACKING.md b/HACKING.md index 5da559a3..574abc77 100644 --- a/HACKING.md +++ b/HACKING.md @@ -74,6 +74,9 @@ Please ensure that you have installed: * [Node Version Manager](https://github.com/nvm-sh/nvm) or Node.js 20 (`lts/iron`) * [Yarn](https://yarnpkg.com/) package manager. * Go 1.22+ +* Protobuf: + * [protoc](https://developers.google.com/protocol-buffers) + * [Protobuf Go plugins](https://grpc.io/docs/languages/go/quickstart/) ### First-time setup diff --git a/build.mk b/build.mk index efce0492..e788769c 100644 --- a/build.mk +++ b/build.mk @@ -81,4 +81,4 @@ build: check-go check-yarn clean preinstall gen build-server wasm go-index impor .PHONY:gen gen: - @find . -name '*_gen.go' -exec go generate -v {} \; + @find . -name 'generate.go' -exec go generate -v {} \; diff --git a/go.mod b/go.mod index d706c0b1..9a50b15e 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,8 @@ require ( github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.8.0 // indirect - golang.org/x/sys v0.3.0 // indirect + golang.org/x/sys v0.21.0 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index cb8fbba4..52c66b1d 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -102,6 +103,12 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From aa745b923e8b3484baead8942a0c1dcaa360f1e0 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 05:03:01 -0400 Subject: [PATCH 20/43] feat: use structure of arrays to save space --- internal/pkgindex/cmd/flags.go | 39 -------- internal/pkgindex/cmd/index.go | 13 ++- internal/pkgindex/docutil/decl.go | 28 +++--- internal/pkgindex/docutil/traverse.go | 17 ++-- internal/pkgindex/docutil/traverse_test.go | 7 +- internal/pkgindex/docutil/types.go | 17 ++++ internal/pkgindex/docutil/value.go | 12 +-- internal/pkgindex/index/parse.go | 20 +++-- internal/pkgindex/index/scanner.go | 12 ++- internal/pkgindex/index/traverse.go | 17 ++-- internal/pkgindex/index/types.go | 100 ++++++++++++++------- 11 files changed, 146 insertions(+), 136 deletions(-) diff --git a/internal/pkgindex/cmd/flags.go b/internal/pkgindex/cmd/flags.go index ab2c8b8a..85ebab63 100644 --- a/internal/pkgindex/cmd/flags.go +++ b/internal/pkgindex/cmd/flags.go @@ -6,11 +6,6 @@ import ( "github.com/x1unix/go-playground/internal/pkgindex/imports" ) -const ( - formatJSON = "json" - formatProto = "proto" -) - type globalFlags struct { goRoot string } @@ -49,37 +44,3 @@ func (f importsFlags) validate() error { return nil } - -type indexFlags struct { - importsFlags - - format string -} - -func (f indexFlags) validate() error { - switch f.format { - case "proto": - if f.stdout { - return fmt.Errorf("--stdout flag not allowed for Protobuf format") - } - if f.prettyPrint { - return fmt.Errorf("pretty print is not avaiable for Protobuf format") - } - if f.outFile == "" { - return fmt.Errorf("missing output file flag") - } - return nil - case "", "json": - if f.outFile == "" && !f.stdout { - return fmt.Errorf("missing output file flag. Use --stdout flag to print into stdout") - } - - if f.stdout && f.outFile != "" { - return fmt.Errorf("ambiguous output flag: --stdout and output file flag can't be together") - } - default: - return fmt.Errorf("unsupported output format %q", f.format) - } - - return nil -} diff --git a/internal/pkgindex/cmd/index.go b/internal/pkgindex/cmd/index.go index d4144331..e96a86c4 100644 --- a/internal/pkgindex/cmd/index.go +++ b/internal/pkgindex/cmd/index.go @@ -8,10 +8,8 @@ import ( ) func newCmdIndex(g *globalFlags) *cobra.Command { - flags := indexFlags{ - importsFlags: importsFlags{ - globalFlags: g, - }, + flags := importsFlags{ + globalFlags: g, } cmd := &cobra.Command{ @@ -26,23 +24,22 @@ func newCmdIndex(g *globalFlags) *cobra.Command { }, } - cmd.Flags().StringVarP(&flags.format, "format", "F", "json", "Output format: proto or json") cmd.Flags().StringVarP(&flags.outFile, "output", "o", "", "Path to output file. When enpty, prints to stdout") cmd.Flags().BoolVarP(&flags.prettyPrint, "pretty", "P", false, "Add indents to JSON output") cmd.Flags().BoolVar(&flags.stdout, "stdout", false, "Dump result into stdout") return cmd } -func runGenIndex(flags indexFlags) error { +func runGenIndex(flags importsFlags) error { entries, err := index.ScanRoot(flags.goRoot) if err != nil { return err } - if err := writeOutput(flags.importsFlags, entries); err != nil { + if err := writeOutput(flags, entries); err != nil { return err } - log.Printf("Scanned %d packages and %d symbols", len(entries.Packages), len(entries.Symbols)) + log.Printf("Scanned %d packages and %d symbols", len(entries.Packages.Names), len(entries.Symbols.Names)) return nil } diff --git a/internal/pkgindex/docutil/decl.go b/internal/pkgindex/docutil/decl.go index 67080eb9..76662039 100644 --- a/internal/pkgindex/docutil/decl.go +++ b/internal/pkgindex/docutil/decl.go @@ -6,16 +6,16 @@ import ( "go/token" ) -// DeclToSymbol constructs symbol from generic type of value spec. -func DeclToSymbol(fset *token.FileSet, specGroup *ast.GenDecl, filter Filter) ([]Symbol, error) { +// CollectDecls collects symbols from generic type of value spec. +func CollectDecls(fset *token.FileSet, specGroup *ast.GenDecl, filter Filter, collector Collector) (count int, err error) { if len(specGroup.Specs) == 0 { - return nil, nil + return 0, nil } filter = filterOrDefault(filter) block, err := NewBlockData(specGroup) if err != nil { - return nil, err + return 0, err } traverseCtx := TraverseContext{ @@ -24,7 +24,6 @@ func DeclToSymbol(fset *token.FileSet, specGroup *ast.GenDecl, filter Filter) ([ Filter: filter, } - completions := make([]Symbol, 0, len(specGroup.Specs)) for _, spec := range specGroup.Specs { switch t := spec.(type) { case *ast.TypeSpec: @@ -34,25 +33,22 @@ func DeclToSymbol(fset *token.FileSet, specGroup *ast.GenDecl, filter Filter) ([ item, err := TypeToSymbol(fset, block, t) if err != nil { - return nil, err + return count, err } - completions = append(completions, item) + count++ + collector.CollectSymbol(item) case *ast.ValueSpec: - items, err := ValueToSymbol(traverseCtx, t) + n, err := CollectValues(traverseCtx, t, collector) if err != nil { - return nil, err + return count, err } - if len(items) == 0 { - continue - } - - completions = append(completions, items...) + count += n default: - return nil, fmt.Errorf("unsupported declaration type %T", t) + return count, fmt.Errorf("unsupported declaration type %T", t) } } - return completions, nil + return count, nil } diff --git a/internal/pkgindex/docutil/traverse.go b/internal/pkgindex/docutil/traverse.go index e4fbbdc7..81710028 100644 --- a/internal/pkgindex/docutil/traverse.go +++ b/internal/pkgindex/docutil/traverse.go @@ -15,12 +15,10 @@ type TraverseOpts struct { SnippetFormat monaco.CompletionItemInsertTextRule } -type TraverseReducer = func(items ...Symbol) - // CollectSymbols traverses root file declarations and transforms them into completion items. // // Important: type methods are ignored. -func CollectSymbols(decls []ast.Decl, opts TraverseOpts, reducer TraverseReducer) error { +func CollectSymbols(decls []ast.Decl, opts TraverseOpts, collector Collector) (count int, err error) { filter := filterOrDefault(opts.Filter) for _, decl := range decls { switch t := decl.(type) { @@ -36,27 +34,28 @@ func CollectSymbols(decls []ast.Decl, opts TraverseOpts, reducer TraverseReducer item, err := SymbolFromFunc(opts.FileSet, t, monaco.InsertAsSnippet) if err != nil { - return fmt.Errorf( + return count, fmt.Errorf( "can't parse function %s: %w (pos: %s)", t.Name.String(), err, GetDeclPosition(opts.FileSet, t), ) } - reducer(item) + count++ + collector.CollectSymbol(item) case *ast.GenDecl: if t.Tok == token.IMPORT { continue } - items, err := DeclToSymbol(opts.FileSet, t, filter) + n, err := CollectDecls(opts.FileSet, t, filter, collector) if err != nil { - return fmt.Errorf( + return count, fmt.Errorf( "can't parse decl %s: %w (at %s)", t.Tok, err, GetDeclPosition(opts.FileSet, t), ) } - reducer(items...) + count += n default: fname := opts.FileSet.File(decl.Pos()).Name() log.Printf( @@ -66,5 +65,5 @@ func CollectSymbols(decls []ast.Decl, opts TraverseOpts, reducer TraverseReducer } } - return nil + return count, nil } diff --git a/internal/pkgindex/docutil/traverse_test.go b/internal/pkgindex/docutil/traverse_test.go index 1412128d..25ecc478 100644 --- a/internal/pkgindex/docutil/traverse_test.go +++ b/internal/pkgindex/docutil/traverse_test.go @@ -56,9 +56,9 @@ func TestTypeToCompletionItem(t *testing.T) { got []Symbol want []Symbol ) - err = CollectSymbols(r.Decls, opts, func(items ...Symbol) { - got = append(got, items...) - }) + n, err := CollectSymbols(r.Decls, opts, CollectorFunc(func(item Symbol) { + got = append(got, item) + })) if c.expectErr != "" { require.Error(t, err) require.Contains(t, err.Error(), c.expectErr) @@ -88,6 +88,7 @@ func TestTypeToCompletionItem(t *testing.T) { }) require.Equal(t, want, got) + require.Equal(t, len(got), n) }) } } diff --git a/internal/pkgindex/docutil/types.go b/internal/pkgindex/docutil/types.go index 249eb90f..7a732b39 100644 --- a/internal/pkgindex/docutil/types.go +++ b/internal/pkgindex/docutil/types.go @@ -23,3 +23,20 @@ type Symbol struct { func (sym Symbol) Compare(b Symbol) int { return strings.Compare(sym.Label, b.Label) } + +// Collector accumulates symbols collected during AST traversal. +// +// Collector allows to reduce memory allocations caused by return slice allocations +// during recursive AST traversal. +type Collector interface { + CollectSymbol(sym Symbol) +} + +// CollectorFunc func type is an adapter to allow the use of ordinary functions as collectors. +// +// If f is a function with the appropriate signature, CollectorFunc(f) is a Collector that calls f. +type CollectorFunc func(sym Symbol) + +func (fn CollectorFunc) CollectSymbol(sym Symbol) { + fn(sym) +} diff --git a/internal/pkgindex/docutil/value.go b/internal/pkgindex/docutil/value.go index 4a344bf7..b8b671ca 100644 --- a/internal/pkgindex/docutil/value.go +++ b/internal/pkgindex/docutil/value.go @@ -12,14 +12,13 @@ type TraverseContext struct { Filter Filter } -// ValueToSymbol constructs completion item from value declaration. +// CollectValues constructs completion item from value declaration. // // Able to handle special edge cases for builtin declarations. -func ValueToSymbol(ctx TraverseContext, spec *ast.ValueSpec) ([]Symbol, error) { +func CollectValues(ctx TraverseContext, spec *ast.ValueSpec, collector Collector) (count int, err error) { filter := filterOrDefault(ctx.Filter) blockDoc := getValueDocumentation(ctx.Block, spec) - items := make([]Symbol, 0, len(spec.Values)) for _, val := range spec.Names { if filter.Ignore(val.Name) { continue @@ -27,7 +26,7 @@ func ValueToSymbol(ctx TraverseContext, spec *ast.ValueSpec) ([]Symbol, error) { detail, err := detailFromIdent(ctx.FileSet, ctx.Block, val) if err != nil { - return nil, err + return count, err } item := Symbol{ @@ -38,10 +37,11 @@ func ValueToSymbol(ctx TraverseContext, spec *ast.ValueSpec) ([]Symbol, error) { Documentation: blockDoc, } - items = append(items, item) + count++ + collector.CollectSymbol(item) } - return items, nil + return count, nil } func getValueDocumentation(block BlockData, spec *ast.ValueSpec) string { diff --git a/internal/pkgindex/index/parse.go b/internal/pkgindex/index/parse.go index 42f0d699..7f718822 100644 --- a/internal/pkgindex/index/parse.go +++ b/internal/pkgindex/index/parse.go @@ -12,15 +12,18 @@ var ignoreBuiltins = docutil.NewIgnoreList( "Type", "Type1", "IntegerType", "FloatType", "ComplexType", ) +type CollectFn = func(src SymbolSource, sym docutil.Symbol) + type sourceSummary struct { - packageName string - doc *ast.CommentGroup - symbols []SymbolInfo + packageName string + symbolsCount int + doc *ast.CommentGroup } type fileParseParams struct { importPath string parseDoc bool + collector CollectFn } func getFilter(importPath string) docutil.Filter { @@ -39,7 +42,6 @@ func parseFile(fset *token.FileSet, fpath string, params fileParseParams) (*sour summary := sourceSummary{ packageName: root.Name.String(), - symbols: make([]SymbolInfo, 0, len(root.Decls)), } // "go/doc" ignores some packages from GOROOT thus it doesn't work for us. @@ -53,17 +55,17 @@ func parseFile(fset *token.FileSet, fpath string, params fileParseParams) (*sour Path: params.importPath, } + collector := docutil.CollectorFunc(func(sym docutil.Symbol) { + params.collector(src, sym) + }) + opts := docutil.TraverseOpts{ FileSet: fset, Filter: getFilter(params.importPath), SnippetFormat: monaco.InsertAsSnippet, } - err = docutil.CollectSymbols(root.Decls, opts, func(items ...docutil.Symbol) { - for _, item := range items { - summary.symbols = append(summary.symbols, IntoSymbolInfo(item, src)) - } - }) + summary.symbolsCount, err = docutil.CollectSymbols(root.Decls, opts, collector) if err != nil { return nil, err diff --git a/internal/pkgindex/index/scanner.go b/internal/pkgindex/index/scanner.go index 37d39f22..4c136d4f 100644 --- a/internal/pkgindex/index/scanner.go +++ b/internal/pkgindex/index/scanner.go @@ -51,8 +51,8 @@ func ScanRoot(goRoot string) (*GoIndexFile, error) { return nil, err } - packages := make([]PackageInfo, 0, pkgBuffSize) - symbols := make([]SymbolInfo, 0, symBuffSize) + packages := NewPackages(pkgBuffSize) + symbols := NewSymbols(symBuffSize) for queue.Occupied() { v, ok := queue.Pop() @@ -69,7 +69,7 @@ func ScanRoot(goRoot string) (*GoIndexFile, error) { continue } - result, err := traverseScanEntry(v, queue) + result, err := traverseScanEntry(v, queue, symbols.Append) if err != nil { return nil, fmt.Errorf("error while scanning package %q: %w", v.importPath, err) } @@ -81,11 +81,9 @@ func ScanRoot(goRoot string) (*GoIndexFile, error) { // Edge case: "builtin" package exists only for documentation purposes // and not importable. // Also skip empty packages (usually part of vendor path). - if result.pkgInfo.ImportPath != docutil.BuiltinPackage && len(result.symbols) > 0 { - packages = append(packages, result.pkgInfo) + if result.pkgInfo.ImportPath != docutil.BuiltinPackage && result.symbolsCount > 0 { + packages.Append(result.pkgInfo) } - - symbols = append(symbols, result.symbols...) } return &GoIndexFile{ diff --git a/internal/pkgindex/index/traverse.go b/internal/pkgindex/index/traverse.go index c1e2d241..e1763c73 100644 --- a/internal/pkgindex/index/traverse.go +++ b/internal/pkgindex/index/traverse.go @@ -11,13 +11,13 @@ import ( ) type traverseResult struct { - pkgInfo PackageInfo - symbols []SymbolInfo + pkgInfo PackageInfo + symbolsCount int } -func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*traverseResult, error) { +func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry], collector CollectFn) (*traverseResult, error) { var ( - symbols []SymbolInfo + count int pkgInfo = PackageInfo{ ImportPath: entry.importPath, } @@ -41,12 +41,13 @@ func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*trave f, err := parseFile(fset, absPath, fileParseParams{ parseDoc: pkgInfo.Doc == "", importPath: entry.importPath, + collector: collector, }) if err != nil { return nil, fmt.Errorf("can't parse file %q: %w", absPath, err) } - symbols = append(symbols, f.symbols...) + count += f.symbolsCount pkgInfo.Name = f.packageName if f.doc != nil { pkgInfo.Doc = docutil.BuildPackageDoc(f.doc, entry.importPath) @@ -76,12 +77,12 @@ func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry]) (*trave } } - if len(symbols) == 0 { + if count == 0 { return nil, nil } return &traverseResult{ - pkgInfo: pkgInfo, - symbols: symbols, + pkgInfo: pkgInfo, + symbolsCount: count, }, nil } diff --git a/internal/pkgindex/index/types.go b/internal/pkgindex/index/types.go index 05b1731b..03399a96 100644 --- a/internal/pkgindex/index/types.go +++ b/internal/pkgindex/index/types.go @@ -18,45 +18,80 @@ type PackageInfo struct { Doc string `json:"doc,omitempty"` } -type SymbolInfo struct { - // Name is symbol name. - Name string `json:"name"` +// Packages is a flat representation of PackageInfo list. +type Packages struct { + Names []string `json:"names"` + Paths []string `json:"paths"` + Docs []string `json:"docs"` +} - // Doc is documentation in Markdown format. - Doc string `json:"doc,omitempty"` +func NewPackages(capacity int) Packages { + return Packages{ + Names: make([]string, 0, capacity), + Paths: make([]string, 0, capacity), + Docs: make([]string, 0, capacity), + } +} - // Detail is symbol summary. - Detail string `json:"detail,omitempty"` +func (pkgs *Packages) Append(pkg PackageInfo) { + pkgs.Names = append(pkgs.Names, pkg.Name) + pkgs.Paths = append(pkgs.Paths, pkg.ImportPath) + pkgs.Docs = append(pkgs.Docs, pkg.Doc) +} + +// Symbols is a flat representation of Go package symbols. +type Symbols struct { + // Names are list of symbol names. + Names []string `json:"names"` + + // Docs are list of symbol documentation. + Docs []string `json:"docs,omitempty"` - // Signature contains type declaration including public fields. - Signature string `json:"signature,omitempty"` + // Details are list of short symbol summaries + Details []string `json:"details,omitempty"` - // InsertText is text to be inserted by completion. - InsertText string `json:"insertText"` + // Signatures contain string representation of a symbol (e.g. struct definition) + // which is visible when user hovers on a symbol. + Signatures []string `json:"signatures,omitempty"` - // InsertTextRules controls InsertText snippet format. - InsertTextRules monaco.CompletionItemInsertTextRule `json:"insertTextRules,omitempty"` + // InsertTexts are values to be inserted when symbol suggestion is selected. + InsertTexts []string `json:"insertTexts"` - // Kind is symbol type. - Kind monaco.CompletionItemKind `json:"kind"` + // InsertTextRules contains snippet insertion rules for InsertTexts. + InsertTextRules []monaco.CompletionItemInsertTextRule `json:"insertTextRules,omitempty"` - // Package contains information where symbol came from. - Package SymbolSource `json:"package"` + // Kinds contains symbol type for suggestion icon. + Kinds []monaco.CompletionItemKind `json:"kinds"` + + // Packages contains information where particular symbol belongs (to what package). + Packages []SymbolSource `json:"packages"` } -func IntoSymbolInfo(item docutil.Symbol, src SymbolSource) SymbolInfo { - return SymbolInfo{ - Name: item.Label, - Doc: item.Documentation, - Detail: item.Detail, - InsertText: item.InsertText, - InsertTextRules: item.InsertTextRules, - Kind: item.Kind, - Signature: item.Signature, - Package: src, +func NewSymbols(capacity int) Symbols { + return Symbols{ + Names: make([]string, 0, capacity), + Docs: make([]string, 0, capacity), + Details: make([]string, 0, capacity), + Signatures: make([]string, 0, capacity), + InsertTexts: make([]string, 0, capacity), + InsertTextRules: make([]monaco.CompletionItemInsertTextRule, 0, capacity), + Kinds: make([]monaco.CompletionItemKind, 0, capacity), + Packages: make([]SymbolSource, 0, capacity), } } +func (s *Symbols) Append(src SymbolSource, sym docutil.Symbol) { + s.Names = append(s.Names, sym.Label) + s.Docs = append(s.Docs, sym.Documentation) + s.Details = append(s.Details, sym.Detail) + s.Signatures = append(s.Signatures, sym.Signature) + s.InsertTexts = append(s.InsertTexts, sym.InsertText) + s.InsertTextRules = append(s.InsertTextRules, sym.InsertTextRules) + s.Kinds = append(s.Kinds, sym.Kind) + s.Packages = append(s.Packages, src) +} + +// SymbolSource holds information where symbol belongs to. type SymbolSource struct { // Name is package name. Name string `json:"name"` @@ -65,6 +100,9 @@ type SymbolSource struct { Path string `json:"path"` } +// GoIndexFile contains flat list of all Go packages and symbols (functions, types and values). +// +// Data is organized into a flat structure (soa) in order to reduce output file size. type GoIndexFile struct { // Version is file format version. Version int `json:"version"` @@ -72,9 +110,9 @@ type GoIndexFile struct { // Go is Go version used to generate index. Go string `json:"go"` - // Packages is list of standard Go packages. - Packages []PackageInfo `json:"packages"` + // Packages is structure of arrays of standard Go packages. + Packages Packages `json:"packages"` - // Symbols is list of all Go symbols. - Symbols []SymbolInfo `json:"symbols"` + // Symbols is structure of arrays of package symbols. + Symbols Symbols `json:"symbols"` } From baa32b5831f2d7f1b06dc0493d7d6eedde35c4bb Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 05:36:32 -0400 Subject: [PATCH 21/43] feat: add tests --- internal/pkgindex/docutil/comments_test.go | 40 ++++++++++++++++++++++ internal/pkgindex/docutil/filter.go | 6 ++-- internal/pkgindex/docutil/filter_test.go | 22 ++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 internal/pkgindex/docutil/filter_test.go diff --git a/internal/pkgindex/docutil/comments_test.go b/internal/pkgindex/docutil/comments_test.go index 6afd9dc2..1e1b5808 100644 --- a/internal/pkgindex/docutil/comments_test.go +++ b/internal/pkgindex/docutil/comments_test.go @@ -31,6 +31,22 @@ func TestIsPackageDoc(t *testing.T) { }, }, }, + "syscall": { + // issue #367 + expect: false, + group: &ast.CommentGroup{ + List: []*ast.Comment{ + { + Slash: 161, + Text: "// mkasm.go generates assembly trampolines to call library routines from Go.", + }, + { + Slash: 238, + Text: "// This program must be run after mksyscall.pl.", + }, + }, + }, + }, } for n, c := range cases { @@ -40,3 +56,27 @@ func TestIsPackageDoc(t *testing.T) { }) } } + +func TestFormatCommentGroup(t *testing.T) { + want := "Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object" + + " (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O." + input := &ast.CommentGroup{ + List: []*ast.Comment{ + { + Slash: 161, + Text: "// Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer", + }, + { + Slash: 238, + Text: "// object, creating another object (Reader or Writer) that also implements", + }, + { + Slash: 313, + Text: "// the interface but provides buffering and some help for textual I/O.", + }, + }, + } + + got := FormatCommentGroup(input) + require.Equal(t, want, string(got)) +} diff --git a/internal/pkgindex/docutil/filter.go b/internal/pkgindex/docutil/filter.go index c9721dde..7ce9110e 100644 --- a/internal/pkgindex/docutil/filter.go +++ b/internal/pkgindex/docutil/filter.go @@ -37,12 +37,12 @@ type composedFilter []Filter func (filters composedFilter) Ignore(typeName string) bool { for _, f := range filters { - if !f.Ignore(typeName) { - return false + if f.Ignore(typeName) { + return true } } - return true + return false } // ComposeFilters allows composing multiple symbol filters into one. diff --git a/internal/pkgindex/docutil/filter_test.go b/internal/pkgindex/docutil/filter_test.go new file mode 100644 index 00000000..7de86333 --- /dev/null +++ b/internal/pkgindex/docutil/filter_test.go @@ -0,0 +1,22 @@ +package docutil + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestComposedFilter_Ignore(t *testing.T) { + filters := ComposeFilters(UnexportedFilter{}, NewIgnoreList("Foo")) + + require.True(t, filters.Ignore("Foo")) + require.True(t, filters.Ignore("bar")) + require.False(t, filters.Ignore("Baz")) +} + +func TestIgnoreList_Ignore(t *testing.T) { + f := NewIgnoreList("Foo", "Bar") + require.True(t, f.Ignore("Foo")) + require.True(t, f.Ignore("Bar")) + require.False(t, f.Ignore("Baz")) +} From 423b818fa61d4fd759aac39e6e69664b14dfe537 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 05:44:57 -0400 Subject: [PATCH 22/43] chore: goimports --- internal/pkgindex/index/traverse.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/pkgindex/index/traverse.go b/internal/pkgindex/index/traverse.go index e1763c73..2433b939 100644 --- a/internal/pkgindex/index/traverse.go +++ b/internal/pkgindex/index/traverse.go @@ -2,12 +2,13 @@ package index import ( "fmt" - "github.com/x1unix/go-playground/internal/pkgindex/docutil" - "github.com/x1unix/go-playground/internal/pkgindex/imports" "go/token" "os" "path" "path/filepath" + + "github.com/x1unix/go-playground/internal/pkgindex/docutil" + "github.com/x1unix/go-playground/internal/pkgindex/imports" ) type traverseResult struct { From 54e1e679168036342067858494ad0b0038dd9e5b Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 06:03:10 -0400 Subject: [PATCH 23/43] chore: add pprof --- internal/pkgindex/cmd/flags.go | 3 ++- internal/pkgindex/cmd/root.go | 30 +++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/internal/pkgindex/cmd/flags.go b/internal/pkgindex/cmd/flags.go index 85ebab63..9c94dbc2 100644 --- a/internal/pkgindex/cmd/flags.go +++ b/internal/pkgindex/cmd/flags.go @@ -7,7 +7,8 @@ import ( ) type globalFlags struct { - goRoot string + goRoot string + heapProfFile string } func (f globalFlags) withDefaults() (globalFlags, error) { diff --git a/internal/pkgindex/cmd/root.go b/internal/pkgindex/cmd/root.go index 47b5d746..17428270 100644 --- a/internal/pkgindex/cmd/root.go +++ b/internal/pkgindex/cmd/root.go @@ -1,6 +1,10 @@ package cmd import ( + "fmt" + "os" + "runtime/pprof" + "github.com/spf13/cobra" ) @@ -11,21 +15,45 @@ func newCmdRoot() *cobra.Command { Use: "pkgindexer [-r goroot] [-o output]", Short: "Go standard library packages scanner", Long: "Tool to generate Go package autocomplete entries for Monaco editor from Go SDK", - PersistentPreRunE: func(c *cobra.Command, args []string) (err error) { + PersistentPreRunE: func(_ *cobra.Command, _ []string) (err error) { *f, err = f.withDefaults() return err }, + PersistentPostRunE: func(_ *cobra.Command, _ []string) error { + return saveHeapProfile(f.heapProfFile) + }, } cmd.PersistentFlags().StringVarP( &f.goRoot, "root", "r", "", "Path to GOROOT. Uses $GOROOT by default", ) + cmd.PersistentFlags().StringVar( + &f.heapProfFile, "prof", "", "Generate a heap profile into a file", + ) cmd.AddCommand(newCmdImports(f)) cmd.AddCommand(newCmdIndex(f)) return cmd } +func saveHeapProfile(outFile string) error { + if outFile == "" { + return nil + } + + f, err := os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to create heap profile file: %w", err) + } + + defer f.Close() + if err := pprof.WriteHeapProfile(f); err != nil { + return fmt.Errorf("failed to write heap profile to %q: %w", outFile, err) + } + + return nil +} + func Run() error { cmd := newCmdRoot() return cmd.Execute() From c01a959da08583c9a94da6ab719d1e83f7aff259 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 06:07:24 -0400 Subject: [PATCH 24/43] chore: fix docs --- HACKING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING.md b/HACKING.md index 574abc77..ce4395cc 100644 --- a/HACKING.md +++ b/HACKING.md @@ -86,7 +86,7 @@ Run following commands to configure a just cloned project: |-------------------|----------------------------------------------------------| | `make preinstall` | Installs NPM packages for a web app. | | `make wasm` | Builds WebAssembly binaries used by the web app. | -| `make pkg-index` | Generates Go packages index for autocomplete in web app. | +| `make go-index` | Generates Go packages index for autocomplete in web app. | ### Running Project From c2154542d49c774ea0a37163c66e9f985bf4dc9e Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 06:56:07 -0400 Subject: [PATCH 25/43] chore: update js types --- web/src/services/completion/types/index.ts | 2 + web/src/services/completion/types/response.ts | 55 +++++++++++++++++++ .../{types.ts => types/suggestion.ts} | 46 ---------------- 3 files changed, 57 insertions(+), 46 deletions(-) create mode 100644 web/src/services/completion/types/index.ts create mode 100644 web/src/services/completion/types/response.ts rename web/src/services/completion/{types.ts => types/suggestion.ts} (69%) diff --git a/web/src/services/completion/types/index.ts b/web/src/services/completion/types/index.ts new file mode 100644 index 00000000..72e0f81a --- /dev/null +++ b/web/src/services/completion/types/index.ts @@ -0,0 +1,2 @@ +export * from './suggestion' +export * from './response' diff --git a/web/src/services/completion/types/response.ts b/web/src/services/completion/types/response.ts new file mode 100644 index 00000000..173839cb --- /dev/null +++ b/web/src/services/completion/types/response.ts @@ -0,0 +1,55 @@ +import type * as monaco from 'monaco-editor' + +interface SymbolSource { + name: string + path: string +} + +/** + * @see internal/pkgindex/index/types.go + */ +export interface Symbols { + names: string[] + docs: string[] + details: string[] + insertTexts: string[] + insertTextRules: monaco.languages.CompletionItemInsertTextRule[] + kinds: monaco.languages.CompletionItemKind[] + packages: SymbolSource[] +} + +/** + * @see internal/pkgindex/index/types.go + */ +export interface Packages { + names: string[] + paths: string[] + docs: string[] +} + +/** + * Go index file response type. + * + * @see internal/pkgindex/index/types.go + */ +export interface GoIndexFile { + /** + * File format version. + */ + version: number + + /** + * Go version used to generate index. + */ + go: string + + /** + * List of standard packages. + */ + packages: Packages + + /** + * List of symbols of each package. + */ + symbols: Symbols +} diff --git a/web/src/services/completion/types.ts b/web/src/services/completion/types/suggestion.ts similarity index 69% rename from web/src/services/completion/types.ts rename to web/src/services/completion/types/suggestion.ts index 226f4d04..d93ce8bd 100644 --- a/web/src/services/completion/types.ts +++ b/web/src/services/completion/types/suggestion.ts @@ -80,49 +80,3 @@ export interface SuggestionQuery { value: string context: SuggestionContext } - -export interface PackageInfo { - name: string - importPath: string - doc?: string -} - -export interface SymbolInfo { - name: string - doc?: string - detail?: string - insertText: string - insertTextRules: monaco.languages.CompletionItemInsertTextRule - kind: monaco.languages.CompletionItemKind - package: { - name: string - path: string - } -} - -/** - * Go index file response type. - * - * Should be in sync with `/internal/pkgindex/index/types.go`! - */ -export interface GoIndexFile { - /** - * File format version. - */ - version: number - - /** - * Go version used to generate index. - */ - go: string - - /** - * List of standard packages. - */ - packages: PackageInfo[] - - /** - * List of symbols of each package. - */ - symbols: SymbolInfo[] -} From f477b4b947d72515fd9edde3bc19a99e9efc8d15 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 13:06:24 -0400 Subject: [PATCH 26/43] fix: respect build constraints during scan --- build.mk | 4 +- go.sum | 1 + internal/pkgindex/cmd/flags.go | 1 + internal/pkgindex/cmd/index.go | 2 + internal/pkgindex/index/scanner.go | 10 ++- internal/pkgindex/index/traverse.go | 116 +++++++++++++++++++--------- 6 files changed, 94 insertions(+), 40 deletions(-) diff --git a/build.mk b/build.mk index e788769c..bc3940b0 100644 --- a/build.mk +++ b/build.mk @@ -35,12 +35,12 @@ check-go: .PHONY: imports-index imports-index: @echo ":: Generating Go imports index..." && \ - $(GO) run ./tools/pkgindexer imports -o $(UI)/public/data/imports.json + $(GO) run ./tools/pkgindexer imports -o $(UI)/public/data/imports.json $(OPTS) .PHONY: go-index go-index: @echo ":: Generating Go symbols index..." && \ - $(GO) run ./tools/pkgindexer index -o $(UI)/public/data/go-index.json + $(GO) run ./tools/pkgindexer index -o $(UI)/public/data/go-index.json $(OPTS) .PHONY:check-yarn check-yarn: diff --git a/go.sum b/go.sum index 52c66b1d..210a157a 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/pkgindex/cmd/flags.go b/internal/pkgindex/cmd/flags.go index 9c94dbc2..9af55416 100644 --- a/internal/pkgindex/cmd/flags.go +++ b/internal/pkgindex/cmd/flags.go @@ -29,6 +29,7 @@ func (f globalFlags) withDefaults() (globalFlags, error) { type importsFlags struct { *globalFlags + verbose bool prettyPrint bool stdout bool outFile string diff --git a/internal/pkgindex/cmd/index.go b/internal/pkgindex/cmd/index.go index e96a86c4..06087983 100644 --- a/internal/pkgindex/cmd/index.go +++ b/internal/pkgindex/cmd/index.go @@ -17,6 +17,7 @@ func newCmdIndex(g *globalFlags) *cobra.Command { Short: "Generate index file with standard Go packages and symbols", Long: "Generate a JSON file that contains list of all standard Go packages and its symbols. Used in new version of app", PreRunE: func(_ *cobra.Command, _ []string) error { + index.Debug = flags.verbose return flags.validate() }, RunE: func(_ *cobra.Command, _ []string) error { @@ -27,6 +28,7 @@ func newCmdIndex(g *globalFlags) *cobra.Command { cmd.Flags().StringVarP(&flags.outFile, "output", "o", "", "Path to output file. When enpty, prints to stdout") cmd.Flags().BoolVarP(&flags.prettyPrint, "pretty", "P", false, "Add indents to JSON output") cmd.Flags().BoolVar(&flags.stdout, "stdout", false, "Dump result into stdout") + cmd.Flags().BoolVarP(&flags.verbose, "verbose", "v", false, "Enable verbose logging") return cmd } diff --git a/internal/pkgindex/index/scanner.go b/internal/pkgindex/index/scanner.go index 4c136d4f..fc77ea9d 100644 --- a/internal/pkgindex/index/scanner.go +++ b/internal/pkgindex/index/scanner.go @@ -19,11 +19,13 @@ const ( // See: Queue.MaxOccupancy queueSize = 120 - // Go 1.23 has 185 packages and over 70k total symbols. - pkgBuffSize = 185 - symBuffSize = 78000 + // Go 1.23 has 182 packages and over 9k total symbols for linux. + pkgBuffSize = 182 + symBuffSize = 9000 ) +var Debug = false + type scanEntry struct { isVendor bool path string @@ -83,6 +85,8 @@ func ScanRoot(goRoot string) (*GoIndexFile, error) { // Also skip empty packages (usually part of vendor path). if result.pkgInfo.ImportPath != docutil.BuiltinPackage && result.symbolsCount > 0 { packages.Append(result.pkgInfo) + } else if Debug { + log.Printf("Skip pkg: %s", result.pkgInfo.ImportPath) } } diff --git a/internal/pkgindex/index/traverse.go b/internal/pkgindex/index/traverse.go index 2433b939..55e583fa 100644 --- a/internal/pkgindex/index/traverse.go +++ b/internal/pkgindex/index/traverse.go @@ -2,7 +2,9 @@ package index import ( "fmt" + "go/build" "go/token" + "log" "os" "path" "path/filepath" @@ -11,11 +13,44 @@ import ( "github.com/x1unix/go-playground/internal/pkgindex/imports" ) +// defaultCtx is build constraint context to select only files +// that match desired platform. +// +// To keep aligned with godoc - uses same GOOS and GOARCH. +var defaultCtx = build.Context{ + GOARCH: "amd64", + GOOS: "linux", +} + type traverseResult struct { pkgInfo PackageInfo symbolsCount int } +func isFileIgnored(entry scanEntry, fname string) (bool, error) { + if !docutil.IsGoSourceFile(fname) { + return true, nil + } + + // provide docs for single-platform packages. + switch entry.importPath { + case "syscall/js": + return false, nil + } + + match, err := defaultCtx.MatchFile(entry.path, fname) + if err != nil { + return false, fmt.Errorf( + "can't check build constraints for file %q: %w", filepath.Join(entry.path, fname), err, + ) + } + + if !match && Debug { + log.Printf("Skip file: %s/%s", entry.importPath, fname) + } + return !match, err +} + func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry], collector CollectFn) (*traverseResult, error) { var ( count int @@ -32,49 +67,34 @@ func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry], collect fset := token.NewFileSet() for _, dirent := range dirents { name := dirent.Name() - absPath := filepath.Join(entry.path, name) - - if !dirent.IsDir() { - if !docutil.IsGoSourceFile(name) { - continue - } - - f, err := parseFile(fset, absPath, fileParseParams{ - parseDoc: pkgInfo.Doc == "", - importPath: entry.importPath, - collector: collector, - }) - if err != nil { - return nil, fmt.Errorf("can't parse file %q: %w", absPath, err) - } - - count += f.symbolsCount - pkgInfo.Name = f.packageName - if f.doc != nil { - pkgInfo.Doc = docutil.BuildPackageDoc(f.doc, entry.importPath) - } + if dirent.IsDir() { + enqueueSubDir(queue, entry, name) continue } - if isDirIgnored(name) { - continue + isIgnored, err := isFileIgnored(entry, name) + if err != nil { + return nil, err } - // TODO: should nested vendors be supported? - if imports.IsVendorDir(name) { - queue.Add(scanEntry{ - isVendor: true, - path: absPath, - }) + if isIgnored { continue } - p := path.Join(entry.importPath, name) - if !isImportPathIgnored(p) { - queue.Add(scanEntry{ - path: absPath, - importPath: p, - }) + absPath := filepath.Join(entry.path, name) + f, err := parseFile(fset, absPath, fileParseParams{ + parseDoc: pkgInfo.Doc == "", + importPath: entry.importPath, + collector: collector, + }) + if err != nil { + return nil, fmt.Errorf("can't parse file %q: %w", absPath, err) + } + + count += f.symbolsCount + pkgInfo.Name = f.packageName + if f.doc != nil { + pkgInfo.Doc = docutil.BuildPackageDoc(f.doc, entry.importPath) } } @@ -87,3 +107,29 @@ func traverseScanEntry(entry scanEntry, queue *imports.Queue[scanEntry], collect symbolsCount: count, }, nil } + +func enqueueSubDir(queue *imports.Queue[scanEntry], parent scanEntry, name string) { + if isDirIgnored(name) { + return + } + + // TODO: should nested vendors be supported? + absPath := filepath.Join(parent.path, name) + if imports.IsVendorDir(name) { + queue.Add(scanEntry{ + isVendor: true, + path: absPath, + }) + return + } + + importPath := path.Join(parent.importPath, name) + if isImportPathIgnored(importPath) { + return + } + + queue.Add(scanEntry{ + path: absPath, + importPath: importPath, + }) +} From 6df3ae77904f89da5e27f0efb16d4b4658f2f7b7 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 13:55:26 -0400 Subject: [PATCH 27/43] feat: flatten symbol source struct --- internal/pkgindex/index/types.go | 12 ++- web/src/services/completion/service.ts | 50 +++++++------ web/src/services/completion/types/response.ts | 9 ++- web/src/services/completion/utils.ts | 73 +++++++++++-------- web/src/services/storage/types/completion.ts | 5 ++ 5 files changed, 91 insertions(+), 58 deletions(-) diff --git a/internal/pkgindex/index/types.go b/internal/pkgindex/index/types.go index 03399a96..c57812e8 100644 --- a/internal/pkgindex/index/types.go +++ b/internal/pkgindex/index/types.go @@ -7,6 +7,8 @@ import ( const GoIndexFileVersion = 1 +type FlatSymbolSource [2]string + type PackageInfo struct { // Name is package name. Name string `json:"name"` @@ -64,7 +66,7 @@ type Symbols struct { Kinds []monaco.CompletionItemKind `json:"kinds"` // Packages contains information where particular symbol belongs (to what package). - Packages []SymbolSource `json:"packages"` + Packages []FlatSymbolSource `json:"packages"` } func NewSymbols(capacity int) Symbols { @@ -76,7 +78,7 @@ func NewSymbols(capacity int) Symbols { InsertTexts: make([]string, 0, capacity), InsertTextRules: make([]monaco.CompletionItemInsertTextRule, 0, capacity), Kinds: make([]monaco.CompletionItemKind, 0, capacity), - Packages: make([]SymbolSource, 0, capacity), + Packages: make([]FlatSymbolSource, 0, capacity), } } @@ -88,7 +90,7 @@ func (s *Symbols) Append(src SymbolSource, sym docutil.Symbol) { s.InsertTexts = append(s.InsertTexts, sym.InsertText) s.InsertTextRules = append(s.InsertTextRules, sym.InsertTextRules) s.Kinds = append(s.Kinds, sym.Kind) - s.Packages = append(s.Packages, src) + s.Packages = append(s.Packages, src.Flatten()) } // SymbolSource holds information where symbol belongs to. @@ -100,6 +102,10 @@ type SymbolSource struct { Path string `json:"path"` } +func (s SymbolSource) Flatten() FlatSymbolSource { + return FlatSymbolSource{s.Name, s.Path} +} + // GoIndexFile contains flat list of all Go packages and symbols (functions, types and values). // // Data is organized into a flat structure (soa) in order to reduce output file size. diff --git a/web/src/services/completion/service.ts b/web/src/services/completion/service.ts index 5bf78c91..c852acb2 100644 --- a/web/src/services/completion/service.ts +++ b/web/src/services/completion/service.ts @@ -3,10 +3,10 @@ import type { GoIndexFile, SuggestionQuery } from './types' import { completionFromPackage, completionFromSymbol, + constructPackages, + constructSymbols, findPackagePathFromContext, importCompletionFromPackage, - intoPackageIndexItem, - intoSymbolIndexItem, } from './utils' import { type SymbolIndexItem } from '~/services/storage/types' @@ -17,6 +17,7 @@ const completionVersionKey = 'completionItems.version' */ export class GoCompletionService { private cachePopulated = false + private populatePromise?: Promise /** * Store keeps completions in cache. @@ -112,28 +113,35 @@ export class GoCompletionService { } private async populateCache() { - const rsp = await fetch('/data/go-index.json') - if (!rsp.ok) { - throw new Error(`${rsp.status} ${rsp.statusText}`) - } + if (!this.populatePromise) { + // Cache population might be triggered by multiple actors outside. + this.populatePromise = (async () => { + const rsp = await fetch('/data/go-index.json') + if (!rsp.ok) { + throw new Error(`${rsp.status} ${rsp.statusText}`) + } - const data: GoIndexFile = await rsp.json() - if (data.version > 1) { - console.warn(`unsupported symbol index version: ${data.version}, skip update.`) - return - } + const data: GoIndexFile = await rsp.json() + if (data.version > 1) { + console.warn(`unsupported symbol index version: ${data.version}, skip update.`) + return + } + + const packages = constructPackages(data.packages) + const symbols = constructSymbols(data.symbols) - const packages = data.packages.map(intoPackageIndexItem) - const symbols = data.symbols.map(intoSymbolIndexItem) + await Promise.all([ + this.db.packageIndex.clear(), + this.db.symbolIndex.clear(), + this.db.packageIndex.bulkAdd(packages), + this.db.symbolIndex.bulkAdd(symbols), + this.keyValue.setItem(completionVersionKey, data.go), + ]) - await Promise.all([ - this.db.packageIndex.clear(), - this.db.symbolIndex.clear(), - this.db.packageIndex.bulkAdd(packages), - this.db.symbolIndex.bulkAdd(symbols), - this.keyValue.setItem(completionVersionKey, data.go), - ]) + this.cachePopulated = true + })() + } - this.cachePopulated = true + await this.populatePromise } } diff --git a/web/src/services/completion/types/response.ts b/web/src/services/completion/types/response.ts index 173839cb..07db520b 100644 --- a/web/src/services/completion/types/response.ts +++ b/web/src/services/completion/types/response.ts @@ -1,10 +1,12 @@ import type * as monaco from 'monaco-editor' -interface SymbolSource { - name: string - path: string +export enum SymbolSourceKey { + Name = 0, + Path = 1, } +type SymbolSource = [name: string, path: string] + /** * @see internal/pkgindex/index/types.go */ @@ -12,6 +14,7 @@ export interface Symbols { names: string[] docs: string[] details: string[] + signatures: string[] insertTexts: string[] insertTextRules: monaco.languages.CompletionItemInsertTextRule[] kinds: monaco.languages.CompletionItemKind[] diff --git a/web/src/services/completion/utils.ts b/web/src/services/completion/utils.ts index ff79015a..7b018c97 100644 --- a/web/src/services/completion/utils.ts +++ b/web/src/services/completion/utils.ts @@ -1,5 +1,5 @@ import type * as monaco from 'monaco-editor' -import { ImportClauseType, type PackageInfo, type SuggestionContext, type SymbolInfo } from './types' +import { ImportClauseType, type Packages, type SuggestionContext, type Symbols, SymbolSourceKey } from './types' import type { PackageIndexItem, SymbolIndexItem } from '../storage/types' type CompletionItem = monaco.languages.CompletionItem @@ -12,37 +12,48 @@ const stubRange = undefined as any as monaco.IRange const packageCompletionKind = 8 -export const intoPackageIndexItem = ({ name, importPath, doc }: PackageInfo): PackageIndexItem => ({ - importPath, - name, - prefix: getPrefix(name), - documentation: doc - ? { - value: doc, - isTrusted: true, - } - : undefined, -}) +const stringToMarkdown = (value: string): monaco.IMarkdownString | undefined => { + if (!value.length) { + return undefined + } -export const intoSymbolIndexItem = ({ - name, - package: pkg, - doc, - ...completion -}: SymbolInfo): SymbolIndexItem => ({ - ...completion, - key: `${pkg.path}.${name}`, - prefix: getPrefix(name), - label: name, - packageName: pkg.name, - packagePath: pkg.path, - documentation: doc - ? { - value: doc, - isTrusted: true, - } - : undefined, -}) + return { + value, + isTrusted: true, + } +} + +export const constructPackages = ({ names, paths, docs }: Packages): PackageIndexItem[] => + names.map((name, i) => ({ + name, + importPath: paths[i], + prefix: getPrefix(names[i]), + documentation: stringToMarkdown(docs[i]), + })) + +export const constructSymbols = ({ + names, + docs, + details, + signatures, + insertTexts, + insertTextRules, + kinds, + packages, +}: Symbols): SymbolIndexItem[] => + names.map((name, i) => ({ + key: `${packages[i][SymbolSourceKey.Path]}.${name}`, + label: name, + detail: details[i], + signature: signatures[i], + kind: kinds[i], + insertText: insertTexts[i], + insertTextRules: insertTextRules[i], + prefix: getPrefix(name), + packageName: packages[i][SymbolSourceKey.Name], + packagePath: packages[i][SymbolSourceKey.Path], + documentation: stringToMarkdown(docs[i]), + })) export const importCompletionFromPackage = ({ importPath, name, documentation }: PackageIndexItem): CompletionItem => ({ label: importPath, diff --git a/web/src/services/storage/types/completion.ts b/web/src/services/storage/types/completion.ts index 90e6b3ac..bdab5ca5 100644 --- a/web/src/services/storage/types/completion.ts +++ b/web/src/services/storage/types/completion.ts @@ -61,4 +61,9 @@ export interface SymbolIndexItem extends NormalizedCompletionItem { * Package name part of package path */ packageName: string + + /** + * Signature represents full symbol signature to show on hover. + */ + signature: string } From 9c962bfd882f07765e48017026a55ad6bcd3f514 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 14:43:22 -0400 Subject: [PATCH 28/43] fix: fix indexing and fallbacks --- web/src/services/completion/service.ts | 3 ++- web/src/services/completion/utils.ts | 4 +++- web/src/services/storage/db.ts | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/web/src/services/completion/service.ts b/web/src/services/completion/service.ts index c852acb2..1586e2b5 100644 --- a/web/src/services/completion/service.ts +++ b/web/src/services/completion/service.ts @@ -68,8 +68,9 @@ export class GoCompletionService { const query: Partial = packagePath ? { packagePath, + prefix: value.toLowerCase(), } - : { packageName, prefix: value } + : { packageName, prefix: value.toLowerCase() } const symbols = await this.db.symbolIndex.where(query).toArray() return symbols.map((symbol) => completionFromSymbol(symbol, context, !!packagePath)) diff --git a/web/src/services/completion/utils.ts b/web/src/services/completion/utils.ts index 7b018c97..60ade8df 100644 --- a/web/src/services/completion/utils.ts +++ b/web/src/services/completion/utils.ts @@ -12,6 +12,8 @@ const stubRange = undefined as any as monaco.IRange const packageCompletionKind = 8 +const fallbackValue = (str: string, defaults?: string | undefined) => (str.length ? str : defaults) + const stringToMarkdown = (value: string): monaco.IMarkdownString | undefined => { if (!value.length) { return undefined @@ -44,7 +46,7 @@ export const constructSymbols = ({ names.map((name, i) => ({ key: `${packages[i][SymbolSourceKey.Path]}.${name}`, label: name, - detail: details[i], + detail: fallbackValue(details[i], name), signature: signatures[i], kind: kinds[i], insertText: insertTexts[i], diff --git a/web/src/services/storage/db.ts b/web/src/services/storage/db.ts index b492858c..7a304eee 100644 --- a/web/src/services/storage/db.ts +++ b/web/src/services/storage/db.ts @@ -15,7 +15,7 @@ export class DatabaseStorage extends Dexie { this.version(2).stores({ keyValue: 'key', packageIndex: 'importPath, prefix, name', - symbolIndex: 'key, packagePath, [prefix+packageName]', + symbolIndex: 'key, packagePath, [prefix+packageName], [packageName+label]', }) } } From 1fb0bc5c0b2467fa887998fe5d491650166803a4 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 17:56:25 -0400 Subject: [PATCH 29/43] chore: make monaco-editor work in vitest --- web/package.json | 3 +- web/src/setupTests.ts | 22 +++- web/vite.config.ts | 17 ++- web/yarn.lock | 269 ++++++++++++++++++++++++++++++++++-------- 4 files changed, 257 insertions(+), 54 deletions(-) diff --git a/web/package.json b/web/package.json index a21c6594..0a802cee 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,7 @@ "@fluentui/react": "^8.52.3", "@monaco-editor/loader": "^1.4.0", "@monaco-editor/react": "^4.6.0", - "@testing-library/jest-dom": "^5.16.2", + "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^12.1.2", "@testing-library/user-event": "^13.5.0", "@types/file-saver": "^2.0.7", @@ -48,6 +48,7 @@ "eslint-plugin-testing-library": "^5.0.1", "eslint-plugin-unused-imports": "^3.0.0", "file-saver": "^2.0.5", + "jsdom": "^25.0.1", "monaco-editor": "^0.45.0", "monaco-vim": "^0.3.4", "prettier": "^3.2.4", diff --git a/web/src/setupTests.ts b/web/src/setupTests.ts index 2eb59b05..03b790a7 100644 --- a/web/src/setupTests.ts +++ b/web/src/setupTests.ts @@ -1,5 +1,17 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom/extend-expect' +import '@testing-library/jest-dom/vitest' +import { vi } from 'vitest' + +// Dependency for monaco-editor tests. +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), // deprecated + removeListener: vi.fn(), // deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}) diff --git a/web/vite.config.ts b/web/vite.config.ts index fed48434..29aedcc9 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,10 +1,11 @@ -import { resolve } from 'path' +import { resolve, join } from 'path' import react from '@vitejs/plugin-react-swc' -import { defineConfig } from 'vite' +import { defineConfig, type UserConfig } from 'vite' import { nodePolyfills } from 'vite-plugin-node-polyfills' import svgr from 'vite-plugin-svgr' import tsConfigPaths from 'vite-tsconfig-paths' import { createHtmlPlugin } from 'vite-plugin-html' +import 'vitest/config' const { NODE_ENV = 'dev', @@ -21,6 +22,18 @@ export default defineConfig({ '~': resolve(__dirname, './src'), }, }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: join(__dirname, 'src/setupTests.ts'), + alias: [ + { + find: /^monaco-editor$/, + replacement: + join(__dirname,'node_modules/monaco-editor/esm/vs/editor/editor.api'), + }, + ], + }, server: { port: 3000, host: '0.0.0.0', diff --git a/web/yarn.lock b/web/yarn.lock index f0053dcc..af8ef1a7 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@adobe/css-tools@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" + integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== + "@ampproject/remapping@^2.0.0": version "2.0.4" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.0.4.tgz#ab4b6d858526ebee0d6c3aa5c3fb56a941c0d7be" @@ -2008,19 +2013,17 @@ lz-string "^1.4.4" pretty-format "^27.0.2" -"@testing-library/jest-dom@^5.16.2": - version "5.16.2" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.2.tgz#f329b36b44aa6149cd6ced9adf567f8b6aa1c959" - integrity sha512-6ewxs1MXWwsBFZXIk4nKKskWANelkdUehchEOokHsN8X7c2eKXGw+77aRV63UU8f/DTSVUPLaGxdrj4lN7D/ug== +"@testing-library/jest-dom@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz#50484da3f80fb222a853479f618a9ce5c47bfe54" + integrity sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA== dependencies: - "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.9.1" + "@adobe/css-tools" "^4.4.0" aria-query "^5.0.0" chalk "^3.0.0" - css "^3.0.0" css.escape "^1.5.1" - dom-accessibility-api "^0.5.6" - lodash "^4.17.15" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" redent "^3.0.0" "@testing-library/react@^12.1.2": @@ -2066,7 +2069,7 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/jest@*", "@types/jest@^27.4.0": +"@types/jest@^27.4.0": version "27.4.0" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed" integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ== @@ -2155,13 +2158,6 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== -"@types/testing-library__jest-dom@^5.9.1": - version "5.14.2" - resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz#564fb2b2dc827147e937a75b639a05d17ce18b44" - integrity sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg== - dependencies: - "@types/jest" "*" - "@typescript-eslint/eslint-plugin@^5.5.0": version "5.10.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.2.tgz#f8c1d59fc37bd6d9d11c97267fdfe722c4777152" @@ -2479,6 +2475,13 @@ acorn@^8.8.2, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -2695,11 +2698,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -atob@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" - integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== - available-typed-arrays@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" @@ -3345,14 +3343,12 @@ css.escape@^1.5.1: resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= -css@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" - integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== +cssstyle@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.1.0.tgz#161faee382af1bafadb6d3867a92a19bcb4aea70" + integrity sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA== dependencies: - inherits "^2.0.4" - source-map "^0.6.1" - source-map-resolve "^0.6.0" + rrweb-cssom "^0.7.1" csstype@^2.2.0: version "2.6.13" @@ -3369,11 +3365,26 @@ damerau-levenshtein@^1.0.7: resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + date-fns@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== +debug@4: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3402,10 +3413,10 @@ debug@^4.3.1, debug@^4.3.4: dependencies: ms "2.1.2" -decode-uri-component@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" - integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== +decimal.js@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== deep-eql@^4.1.3: version "4.1.4" @@ -3502,11 +3513,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.9: version "0.5.11" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.11.tgz#79d5846c4f90eba3e617d9031e921de9324f84ed" integrity sha512-7X6GvzjYf4yTdRKuCVScV+aA9Fvh5r8WzWrXBH9w82ZWB/eYDMGCnazoC/YAqAzUJWHzLOnZqr46K3iEyUhUvw== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-serializer@^1.0.1: version "1.4.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" @@ -4669,6 +4685,13 @@ hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + html-minifier-terser@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" @@ -4682,16 +4705,39 @@ html-minifier-terser@^6.1.0: relateurl "^0.2.7" terser "^5.10.0" +http-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + https-browserify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== +https-proxy-agent@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz#9e8b5013873299e11fab6fd548405da2d6c602b2" + integrity sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw== + dependencies: + agent-base "^7.0.2" + debug "4" + human-signals@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + idb@^7.0.1: version "7.1.1" resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" @@ -4921,6 +4967,11 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -5076,6 +5127,33 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsdom@^25.0.1: + version "25.0.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef" + integrity sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw== + dependencies: + cssstyle "^4.1.0" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.5" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.12" + parse5 "^7.1.2" + rrweb-cssom "^0.7.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^5.0.0" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.18.0" + xml-name-validator "^5.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -5200,7 +5278,7 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash@^4.17.15, lodash@^4.17.21: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5402,7 +5480,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1: +ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -5495,6 +5573,11 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" +nwsapi@^2.2.12: + version "2.2.13" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.13.tgz#e56b4e98960e7a040e5474536587e599c4ff4655" + integrity sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ== + object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -5740,6 +5823,13 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse5@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + pascal-case@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" @@ -5946,6 +6036,11 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + qs@^6.11.2: version "6.11.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" @@ -6289,6 +6384,11 @@ rollup@^4.20.0: "@rollup/rollup-win32-x64-msvc" "4.22.4" fsevents "~2.3.2" +rrweb-cssom@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b" + integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -6325,11 +6425,18 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.2.2" is-regex "^1.1.4" -safer-buffer@^2.1.0: +"safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" @@ -6447,14 +6554,6 @@ source-map-js@^1.2.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== -source-map-resolve@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" - integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== - dependencies: - atob "^2.1.2" - decode-uri-component "^0.2.0" - source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -6468,7 +6567,7 @@ source-map@^0.5.0: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: +source-map@^0.6.0, source-map@~0.6.0: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -6650,6 +6749,11 @@ svg-parser@^2.0.4: resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + synckit@^0.8.6: version "0.8.8" resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" @@ -6710,6 +6814,18 @@ tinyspy@^2.2.0: resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.1.tgz#117b2342f1f38a0dbdcc73a50a454883adf861d1" integrity sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A== +tldts-core@^6.1.49: + version "6.1.49" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.49.tgz#c321ef54b58c067a6b12d229f09d6cb6155c115f" + integrity sha512-ctRO/wzBasOCxAStJG/60Qe8/QpGmaVPsE8djdk0vioxN4uCOgKoveH71Qc2EOmVMIjVf0BjigI5p9ZDuLOygg== + +tldts@^6.1.32: + version "6.1.49" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.49.tgz#fa396141c50c0d234bb8c37b238f47ff4fd874dc" + integrity sha512-E5se9HuCyfwWvmc0JiXiocOw+Cm4tlJCKewdB5RKMH8MmtiTsQCc+yu5BBYB5ZN4lNbz8Xg65bqJ1odS9+RhIA== + dependencies: + tldts-core "^6.1.49" + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -6727,6 +6843,20 @@ toggle-selection@^1.0.6: resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= +tough-cookie@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" + integrity sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q== + dependencies: + tldts "^6.1.32" + +tr46@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" + integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== + dependencies: + punycode "^2.3.1" + ts-api-utils@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" @@ -7071,11 +7201,43 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + web-vitals@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c" integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg== +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +whatwg-url@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.0.0.tgz#00baaa7fd198744910c4b1ef68378f2200e4ceb6" + integrity sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw== + dependencies: + tr46 "^5.0.0" + webidl-conversions "^7.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -7248,6 +7410,21 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + xtend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" From e1bc8f63526052a7168efb842c6b09c1fd00f3c9 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 19:03:12 -0400 Subject: [PATCH 30/43] fix: fix imports traversal --- .../CodeEditor/autocomplete/cache.ts | 4 +- .../CodeEditor/autocomplete/parse.test.ts | 61 ++++++++++++ .../CodeEditor/autocomplete/parse.ts | 95 +++++++++++++------ .../autocomplete/testdata/grouped.txt | 9 ++ web/src/services/completion/service.ts | 5 +- .../services/completion/types/suggestion.ts | 5 + 6 files changed, 146 insertions(+), 33 deletions(-) create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/testdata/grouped.txt diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts index a5f2bce1..0c8c8144 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts @@ -54,7 +54,8 @@ export class DocumentMetadataCache { */ getMetadata(fileName: string, model: monaco.editor.ITextModel) { const data = this.cache.get(fileName) - if (data) { + if (data && !data.hasError) { + console.log('use cache', data) return data } @@ -63,6 +64,7 @@ export class DocumentMetadataCache { private updateCache(fileName: string, model: monaco.editor.ITextModel) { const context = buildImportContext(model) + console.log('cache empty, update meta', context) this.cache.set(fileName, context) return context } diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts new file mode 100644 index 00000000..e9fd3371 --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts @@ -0,0 +1,61 @@ +import * as monaco from 'monaco-editor' +import path from 'path' +import { assert, test, describe } from 'vitest' +import { importContextFromTokens } from './parse' +import type { ImportsContext } from '~/services/completion' + +// Core language packs aren't loaded in vitest. +// Autoloading via import also doesn't work. +import { language } from 'monaco-editor/esm/vs/basic-languages/go/go' + +// eslint-disable-next-line -- vite can't resolve node ESM imports and TS can't resolve CJS types. +const fs = require('fs/promises') as typeof import('fs/promises') + +const baseDir = path.dirname(new URL(import.meta.url).pathname) +const getFixture = async (filename: string) => { + const fpath = path.join(baseDir, 'testdata', filename) + return await fs.readFile(fpath, { encoding: 'utf-8' }) +} + +interface TestCase { + label: string + sourceFile: string + want: ImportsContext +} + +const cases: TestCase[] = [ + { + label: 'should parse group imports', + sourceFile: 'grouped.txt', + want: { + hasError: false, + allPaths: new Set(['fmt']), + blockPaths: ['fmt'], + blockType: 2, + range: { + startLineNumber: 2, + startColumn: 1, + endLineNumber: 5, + endColumn: 2, + }, + totalRange: { + startLineNumber: 1, + endLineNumber: 5, + }, + }, + }, +] + +describe.each(cases)('buildImportContext', ({ label, sourceFile, want }) => { + monaco.languages.register({ id: 'go' }) + monaco.languages.setMonarchTokensProvider('go', language) + + test(label, async () => { + const input = await getFixture(sourceFile) + const model = monaco.editor.createModel(input, 'go', monaco.Uri.file('/file.go')) + const tokens = monaco.editor.tokenize(model.getValue(), model.getLanguageId()) + + const ctx = importContextFromTokens(model, tokens) + assert.deepEqual(want, ctx) + }) +}) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts index 42ac03c9..9e30e0ca 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts @@ -57,7 +57,7 @@ const findPackageBlock = (tokens: Tokens) => { interface ImportHeader { line: number - foundParent?: boolean + hasOpenParen?: boolean argTokens?: monaco.Token[] } @@ -89,7 +89,7 @@ const findImportHeader = (offset: number, tokens: Tokens): ImportHeader | null = switch (rest[k].type) { case GoToken.Parenthesis: - return { line: i, foundParent: true } + return { line: i, hasOpenParen: true } case GoToken.Ident: case GoToken.String: // probably it's a single-line import. @@ -121,26 +121,30 @@ const unquote = (str: string) => { return str } -const readToken = (line: number, tok: monaco.Token, model: monaco.editor.ITextModel): string => { - const word = model.getWordAtPosition({ - lineNumber: line + 1, - column: tok.offset + 1, - })?.word - if (!word) { - throw new ParseError(line, tok.offset, 'parseToken: invalid range') +interface ReadTokenParams { + line: number + tokens: monaco.Token[] + model: monaco.editor.ITextModel +} + +const readToken = (index: number, { line, tokens, model }: ReadTokenParams): string => { + const token = tokens[index] + const endIdx = tokens[index + 1]?.offset + const word = model.getLineContent(line + 1) + if (!word || !token) { + throw new ParseError(line, token.offset, 'parseToken: invalid range') } - return word + return word.slice(token.offset, endIdx) } -const checkParenthesis = (line: number, token: monaco.Token, model: monaco.editor.ITextModel) => { - const isParent = token.type === GoToken.Parenthesis - let isClose = false - if (isParent) { - isClose = readToken(line, token, model) === ')' - } +const checkParenthesis = (index: number, params: ReadTokenParams) => { + const { tokens } = params + const isParen = tokens[index].type === GoToken.Parenthesis + const value = readToken(index, params) - return { isParent, isClose } + const isClose = isParen && value === ')' + return { isParen, isClose, value } } interface ImportBlock { @@ -162,18 +166,24 @@ interface ImportRow { const readImportLine = (line: number, model: monaco.editor.ITextModel, row: monaco.Token[]): ImportStmt | null => { const i = row.findIndex(isNotEmptyToken) const token = row[i] + const params: ReadTokenParams = { + line: line, + tokens: row, + model: model, + } switch (token.type) { case GoToken.Ident: { - const ident = readToken(line, token, model) - const pathTok = row.find(isNotEmptyToken) - if (!pathTok) { + const ident = readToken(i, params) + const pathPos = row.findIndex(isNotEmptyToken) + if (pathPos === -1) { throw new ParseError(line, i, 'missing import path after ident') } - return { alias: ident, path: readToken(line, pathTok, model) } + + return { alias: ident, path: readToken(pathPos, params) } } case GoToken.String: return { - path: readToken(line, token, model), + path: readToken(i, params), } default: throw new UnexpectedTokenError(line, token) @@ -191,9 +201,14 @@ const readImportBlockLine = (line: number, model: monaco.editor.ITextModel, row: } const token = slice[i] + const params: ReadTokenParams = { + line, + model, + tokens: slice + } slice = slice.slice(i + 1) - const { isParent, isClose } = checkParenthesis(line, token, model) - if (isParent) { + const { isParen, isClose, value } = checkParenthesis(i, params) + if (isParen) { if (lastIdent) { throw new UnexpectedTokenError(line, token) } @@ -213,14 +228,15 @@ const readImportBlockLine = (line: number, model: monaco.editor.ITextModel, row: throw new UnexpectedTokenError(line, token) } - lastIdent = readToken(line, token, model) + lastIdent = value + // lastIdent = readToken(line, token, model) break } case GoToken.Comment: { break } case GoToken.String: { - const path = unquote(readToken(line, token, model)) + const path = unquote(value.trim()) if (path) { imports.push(lastIdent ? { path, alias: lastIdent } : { path }) } @@ -242,7 +258,7 @@ const traverseImportGroup = ( header: ImportHeader, tokens: Tokens, ): ImportBlock | null => { - let groupStartFound = header.foundParent ?? false + let groupStartFound = header.hasOpenParen ?? false const imports: ImportStmt[] = [] const range = { startLineNumber: header.line, @@ -258,9 +274,15 @@ const traverseImportGroup = ( continue } + const params: ReadTokenParams = { + model, + line: i, + tokens: row, + } + const token = row[j] - const { isParent, isClose } = checkParenthesis(i, token, model) - if (isParent) { + const { isParen, isClose } = checkParenthesis(j, params) + if (isParen) { if (groupStartFound && isClose) { range.endLineNumber = i + 1 range.endColumn = token.offset + 2 @@ -335,7 +357,17 @@ const findImportBlock = (offset: number, model: monaco.editor.ITextModel, tokens */ export const buildImportContext = (model: monaco.editor.ITextModel): ImportsContext => { const tokens = monaco.editor.tokenize(model.getValue(), model.getLanguageId()) + return importContextFromTokens(model, tokens) +} +/** + * Builds import context from raw tokenized source and model. + * + * This is a workaround function for vitest as Monaco language pack can't be loaded in jsdom env. + * + * Regular users should use `buildImportContext` instead. + */ +export const importContextFromTokens = (model: monaco.editor.ITextModel, tokens: monaco.Token[][]): ImportsContext => { const packagePos = findPackageBlock(tokens) if (packagePos === -1) { // Invalid syntax, discard any import suggestions. @@ -366,7 +398,8 @@ export const buildImportContext = (model: monaco.editor.ITextModel): ImportsCont break } - offset = block.range.endLineNumber + 1 + // returned line starts from 1, keep as is as we count from 0. + offset = block.range.endLineNumber lastImportBlock = block allImports.push(...block.imports.map(({ path }) => path)) } catch (err) { @@ -378,6 +411,7 @@ export const buildImportContext = (model: monaco.editor.ITextModel): ImportsCont if (lastImportBlock) { // TODO: support named imports return { + hasError, allPaths: new Set(allImports), blockPaths: lastImportBlock.imports.map(({ path }) => path), blockType: lastImportBlock.isMultiline ? ImportClauseType.Block : ImportClauseType.Single, @@ -392,6 +426,7 @@ export const buildImportContext = (model: monaco.editor.ITextModel): ImportsCont if (hasError) { // syntax error at first import block, skip return { + hasError, blockType: ImportClauseType.None, } } diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/grouped.txt b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/grouped.txt new file mode 100644 index 00000000..2ca0d5e0 --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/grouped.txt @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, World!") +} diff --git a/web/src/services/completion/service.ts b/web/src/services/completion/service.ts index 1586e2b5..7d1a2bfc 100644 --- a/web/src/services/completion/service.ts +++ b/web/src/services/completion/service.ts @@ -65,12 +65,13 @@ export class GoCompletionService { // to avoid overlap with packages with eponymous name. const packagePath = findPackagePathFromContext(context, packageName) + const prefix = value.toLowerCase() const query: Partial = packagePath ? { packagePath, - prefix: value.toLowerCase(), + prefix, } - : { packageName, prefix: value.toLowerCase() } + : { packageName, prefix } const symbols = await this.db.symbolIndex.where(query).toArray() return symbols.map((symbol) => completionFromSymbol(symbol, context, !!packagePath)) diff --git a/web/src/services/completion/types/suggestion.ts b/web/src/services/completion/types/suggestion.ts index d93ce8bd..89cb93c7 100644 --- a/web/src/services/completion/types/suggestion.ts +++ b/web/src/services/completion/types/suggestion.ts @@ -18,6 +18,11 @@ export enum ImportClauseType { } export interface ImportsContext { + /** + * Whether any error was detected during context build. + */ + hasError?: boolean + /** * List of import paths from all import blocks. */ From f3c1c163ac15e7bbd4342d3bd5db3731d6b28fcc Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 20:13:06 -0400 Subject: [PATCH 31/43] fix: fix single import parse --- web/package.json | 6 +- .../CodeEditor/autocomplete/parse.test.ts | 130 ++- .../CodeEditor/autocomplete/parse.ts | 38 +- .../autocomplete/testdata/corrupted.txt | 10 + .../autocomplete/testdata/grouped.txt | 1 + .../autocomplete/testdata/multiple.txt | 12 + .../autocomplete/testdata/single.txt | 7 + web/yarn.lock | 760 +++++++++++------- 8 files changed, 614 insertions(+), 350 deletions(-) create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/testdata/corrupted.txt create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/testdata/multiple.txt create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/testdata/single.txt diff --git a/web/package.json b/web/package.json index 0a802cee..ecfdf68c 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "@typescript-eslint/eslint-plugin": "^7.16.0", "@typescript-eslint/parser": "^7.16.0", "@vitejs/plugin-react-swc": "^3.5.0", + "@vitest/coverage-v8": "^2.1.2", "@xterm/addon-canvas": "^0.6.0-beta.1", "@xterm/addon-fit": "^0.9.0-beta.1", "@xterm/addon-image": "^0.7.0-beta.1", @@ -69,6 +70,7 @@ "vite-plugin-static-copy": "^1.0.1", "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^4.3.1", + "vitest": "^2.1.2", "web-vitals": "^2.1.4", "workbox-background-sync": "^6.5.4", "workbox-broadcast-update": "^6.5.4", @@ -109,7 +111,5 @@ "last 1 safari version" ] }, - "devDependencies": { - "vitest": "^1.6.0" - } + "devDependencies": {} } diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts index e9fd3371..e3b770f5 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts @@ -1,8 +1,8 @@ import * as monaco from 'monaco-editor' import path from 'path' -import { assert, test, describe } from 'vitest' +import { assert, describe, test } from 'vitest' import { importContextFromTokens } from './parse' -import type { ImportsContext } from '~/services/completion' +import { ImportClauseType, type ImportsContext } from '~/services/completion' // Core language packs aren't loaded in vitest. // Autoloading via import also doesn't work. @@ -18,44 +18,110 @@ const getFixture = async (filename: string) => { } interface TestCase { - label: string sourceFile: string want: ImportsContext } -const cases: TestCase[] = [ - { - label: 'should parse group imports', - sourceFile: 'grouped.txt', - want: { - hasError: false, - allPaths: new Set(['fmt']), - blockPaths: ['fmt'], - blockType: 2, - range: { - startLineNumber: 2, - startColumn: 1, - endLineNumber: 5, - endColumn: 2, - }, - totalRange: { - startLineNumber: 1, - endLineNumber: 5, - }, - }, - }, -] +const runContextTest = async ({ sourceFile, want }: TestCase) => { + const input = await getFixture(sourceFile) + const model = monaco.editor.createModel(input, 'go', monaco.Uri.file(sourceFile)) + const tokens = monaco.editor.tokenize(model.getValue(), model.getLanguageId()) -describe.each(cases)('buildImportContext', ({ label, sourceFile, want }) => { + const ctx = importContextFromTokens(model, tokens) + model.dispose() + + assert.deepEqual(ctx, want) +} + +describe('buildImportContest', () => { monaco.languages.register({ id: 'go' }) monaco.languages.setMonarchTokensProvider('go', language) - test(label, async () => { - const input = await getFixture(sourceFile) - const model = monaco.editor.createModel(input, 'go', monaco.Uri.file('/file.go')) - const tokens = monaco.editor.tokenize(model.getValue(), model.getLanguageId()) + test('should support inline import', async () => { + await runContextTest({ + sourceFile: 'single.txt', + want: { + hasError: false, + allPaths: new Set(['fmt']), + blockPaths: ['fmt'], + blockType: ImportClauseType.Single, + range: { + startLineNumber: 3, + startColumn: 1, + endLineNumber: 3, + endColumn: 13, + }, + totalRange: { + startLineNumber: 1, + endLineNumber: 3, + }, + }, + }) + }) + + test('should parse single group', async () => { + await runContextTest({ + sourceFile: 'grouped.txt', + want: { + hasError: false, + allPaths: new Set(['fmt', 'bar']), + blockPaths: ['fmt', 'bar'], + blockType: ImportClauseType.Block, + range: { + startLineNumber: 2, + startColumn: 1, + endLineNumber: 6, + endColumn: 2, + }, + totalRange: { + startLineNumber: 1, + endLineNumber: 6, + }, + }, + }) + }) - const ctx = importContextFromTokens(model, tokens) - assert.deepEqual(want, ctx) + test('should support multiple import blocks', async () => { + await runContextTest({ + sourceFile: 'multiple.txt', + want: { + hasError: false, + allPaths: new Set(['fmt', 'bar', 'baz']), + blockPaths: ['baz'], + blockType: ImportClauseType.Single, + range: { + startLineNumber: 8, + startColumn: 1, + endLineNumber: 8, + endColumn: 19, + }, + totalRange: { + startLineNumber: 1, + endLineNumber: 8, + }, + }, + }) + }) + + test('should be able to handle partial files', async () => { + await runContextTest({ + sourceFile: 'corrupted.txt', + want: { + hasError: true, + allPaths: new Set(['fmt']), + blockPaths: ['fmt'], + blockType: ImportClauseType.Single, + range: { + startLineNumber: 3, + endLineNumber: 3, + startColumn: 1, + endColumn: 13, + }, + totalRange: { + startLineNumber: 1, + endLineNumber: 3, + }, + }, + }) }) }) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts index 9e30e0ca..6e8babb7 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts @@ -56,7 +56,7 @@ const findPackageBlock = (tokens: Tokens) => { } interface ImportHeader { - line: number + rowIndex: number hasOpenParen?: boolean argTokens?: monaco.Token[] } @@ -84,17 +84,17 @@ const findImportHeader = (offset: number, tokens: Tokens): ImportHeader | null = const rest = row.slice(j + 1) const k = rest.findIndex(isNotEmptyToken) if (k === -1) { - return { line: i } + return { rowIndex: i } } switch (rest[k].type) { case GoToken.Parenthesis: - return { line: i, hasOpenParen: true } + return { rowIndex: i, hasOpenParen: true } case GoToken.Ident: case GoToken.String: // probably it's a single-line import. return { - line: i, + rowIndex: i, argTokens: rest.slice(k), } default: @@ -174,16 +174,22 @@ const readImportLine = (line: number, model: monaco.editor.ITextModel, row: mona switch (token.type) { case GoToken.Ident: { const ident = readToken(i, params) - const pathPos = row.findIndex(isNotEmptyToken) + const restTokens = row.slice(i + 1) + const pathPos = restTokens.findIndex(isNotEmptyToken) if (pathPos === -1) { throw new ParseError(line, i, 'missing import path after ident') } - return { alias: ident, path: readToken(pathPos, params) } + const importPath = readToken(pathPos, { + ...params, + tokens: restTokens, + }).trim() + + return { alias: ident, path: unquote(importPath) } } case GoToken.String: return { - path: readToken(i, params), + path: unquote(readToken(i, params).trim()), } default: throw new UnexpectedTokenError(line, token) @@ -261,13 +267,13 @@ const traverseImportGroup = ( let groupStartFound = header.hasOpenParen ?? false const imports: ImportStmt[] = [] const range = { - startLineNumber: header.line, + startLineNumber: header.rowIndex, startColumn: 1, endLineNumber: -1, endColumn: -1, } - for (let i = header.line + 1; i < tokens.length; i++) { + for (let i = header.rowIndex + 1; i < tokens.length; i++) { const row = tokens[i] const j = row.findIndex(isNotEmptyToken) if (j === -1) { @@ -320,7 +326,7 @@ const traverseImportGroup = ( } } - throw new ParseError(header.line, 1, 'unterminated import block') + throw new ParseError(header.rowIndex, 1, 'unterminated import block') } const findImportBlock = (offset: number, model: monaco.editor.ITextModel, tokens: Tokens): ImportBlock | null => { @@ -335,18 +341,20 @@ const findImportBlock = (offset: number, model: monaco.editor.ITextModel, tokens } // single line import - const importStmt = readImportLine(header.line, model, header.argTokens) + const importStmt = readImportLine(header.rowIndex, model, header.argTokens) if (!importStmt) { // syntax error. return null } + // monaco lines start at 1 + const lineNo = header.rowIndex + 1 return { range: { - startLineNumber: header.line, - endLineNumber: header.line, + startLineNumber: lineNo, + endLineNumber: lineNo, startColumn: 1, - endColumn: header.argTokens[header.argTokens.length - 1].offset, + endColumn: model.getLineLength(lineNo) + 1, }, imports: [importStmt], } @@ -400,6 +408,8 @@ export const importContextFromTokens = (model: monaco.editor.ITextModel, tokens: // returned line starts from 1, keep as is as we count from 0. offset = block.range.endLineNumber + // offset = block.range.endLineNumber + (block.isMultiline ? 0 : 1) + // offset = block.range.endLineNumber + 1 lastImportBlock = block allImports.push(...block.imports.map(({ path }) => path)) } catch (err) { diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/corrupted.txt b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/corrupted.txt new file mode 100644 index 00000000..189616ec --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/corrupted.txt @@ -0,0 +1,10 @@ +package main + +import "fmt" + +import ( + "bar" + +func main() { + fmt.Println("Hello, World!") +} diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/grouped.txt b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/grouped.txt index 2ca0d5e0..98a01861 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/grouped.txt +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/grouped.txt @@ -2,6 +2,7 @@ package main import ( "fmt" + "bar" ) func main() { diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/multiple.txt b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/multiple.txt new file mode 100644 index 00000000..42f243aa --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/multiple.txt @@ -0,0 +1,12 @@ +package main + +import ( + "fmt" + "bar" +) + +import alias "baz" + +func main() { + fmt.Println("Hello, World!") +} diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/single.txt b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/single.txt new file mode 100644 index 00000000..1b0e6be1 --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/single.txt @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("test") +} \ No newline at end of file diff --git a/web/yarn.lock b/web/yarn.lock index af8ef1a7..a4ccce0a 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -27,6 +27,14 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@ampproject/remapping@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" @@ -391,6 +399,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== +"@babel/helper-string-parser@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz#d50e8d37b1176207b4fe9acedec386c565a44a54" + integrity sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g== + "@babel/helper-validator-identifier@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" @@ -401,6 +414,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz#77b7f60c40b15c97df735b38a66ba1d7c3e93da5" + integrity sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg== + "@babel/helper-validator-option@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz#b203ce62ce5fe153899b617c08957de860de4d23" @@ -481,6 +499,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== +"@babel/parser@^7.25.4": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.7.tgz#99b927720f4ddbfeb8cd195a363ed4532f87c590" + integrity sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw== + dependencies: + "@babel/types" "^7.25.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz#4eda6d6c2a0aa79c70fa7b6da67763dfe2141050" @@ -1293,6 +1318,20 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.25.4", "@babel/types@^7.25.7": + version "7.25.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.7.tgz#1b7725c1d3a59f328cb700ce704c46371e6eef9b" + integrity sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ== + dependencies: + "@babel/helper-string-parser" "^7.25.7" + "@babel/helper-validator-identifier" "^7.25.7" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + "@esbuild/aix-ppc64@0.21.5": version "0.21.5" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" @@ -1603,12 +1642,22 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== dependencies: - "@sinclair/typebox" "^0.27.8" + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== "@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" @@ -1619,6 +1668,15 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" @@ -1634,6 +1692,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + "@jridgewell/source-map@^0.3.3": version "0.3.5" resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" @@ -1652,6 +1715,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + "@jridgewell/trace-mapping@^0.3.0": version "0.3.2" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.2.tgz#e051581782a770c30ba219634f2019241c5d3cde" @@ -1668,6 +1736,14 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.23", "@jridgewell/trace-mapping@^0.3.24": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.14" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" @@ -1716,6 +1792,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@pkgr/core@^0.1.0": version "0.1.1" resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" @@ -1832,11 +1913,6 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz#7f698254aadf921e48dda8c0a6b304026b8a9323" integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== -"@sinclair/typebox@^0.27.8": - version "0.27.8" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" - integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== - "@svgr/babel-plugin-add-jsx-attribute@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" @@ -2384,49 +2460,82 @@ dependencies: "@swc/core" "^1.3.96" -"@vitest/expect@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.6.0.tgz#0b3ba0914f738508464983f4d811bc122b51fb30" - integrity sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ== +"@vitest/coverage-v8@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-2.1.2.tgz#0fd098a80c6bda8fb6b8eb65b41be98286331a0a" + integrity sha512-b7kHrFrs2urS0cOk5N10lttI8UdJ/yP3nB4JYTREvR5o18cR99yPpK4gK8oQgI42BVv0ILWYUSYB7AXkAUDc0g== + dependencies: + "@ampproject/remapping" "^2.3.0" + "@bcoe/v8-coverage" "^0.2.3" + debug "^4.3.6" + istanbul-lib-coverage "^3.2.2" + istanbul-lib-report "^3.0.1" + istanbul-lib-source-maps "^5.0.6" + istanbul-reports "^3.1.7" + magic-string "^0.30.11" + magicast "^0.3.4" + std-env "^3.7.0" + test-exclude "^7.0.1" + tinyrainbow "^1.2.0" + +"@vitest/expect@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.2.tgz#e92fa284b8472548f72cacfe896020c64af6bf78" + integrity sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg== dependencies: - "@vitest/spy" "1.6.0" - "@vitest/utils" "1.6.0" - chai "^4.3.10" + "@vitest/spy" "2.1.2" + "@vitest/utils" "2.1.2" + chai "^5.1.1" + tinyrainbow "^1.2.0" -"@vitest/runner@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.6.0.tgz#a6de49a96cb33b0e3ba0d9064a3e8d6ce2f08825" - integrity sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg== +"@vitest/mocker@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.2.tgz#08853a9d8d12afba284aebdf9b5ea26ddae5f20a" + integrity sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA== dependencies: - "@vitest/utils" "1.6.0" - p-limit "^5.0.0" - pathe "^1.1.1" + "@vitest/spy" "^2.1.0-beta.1" + estree-walker "^3.0.3" + magic-string "^0.30.11" -"@vitest/snapshot@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.6.0.tgz#deb7e4498a5299c1198136f56e6e0f692e6af470" - integrity sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ== +"@vitest/pretty-format@2.1.2", "@vitest/pretty-format@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.2.tgz#42882ea18c4cd40428e34f74bbac706a82465193" + integrity sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA== dependencies: - magic-string "^0.30.5" - pathe "^1.1.1" - pretty-format "^29.7.0" + tinyrainbow "^1.2.0" -"@vitest/spy@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.6.0.tgz#362cbd42ccdb03f1613798fde99799649516906d" - integrity sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw== +"@vitest/runner@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.2.tgz#14da1f5eac43fbd9a37d7cd72de102e8f785d727" + integrity sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw== dependencies: - tinyspy "^2.2.0" + "@vitest/utils" "2.1.2" + pathe "^1.1.2" -"@vitest/utils@1.6.0": - version "1.6.0" - resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.6.0.tgz#5c5675ca7d6f546a7b4337de9ae882e6c57896a1" - integrity sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw== +"@vitest/snapshot@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.2.tgz#e20bd794b33fdcd4bfe69138baac7bb890c4d51f" + integrity sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA== dependencies: - diff-sequences "^29.6.3" - estree-walker "^3.0.3" - loupe "^2.3.7" - pretty-format "^29.7.0" + "@vitest/pretty-format" "2.1.2" + magic-string "^0.30.11" + pathe "^1.1.2" + +"@vitest/spy@2.1.2", "@vitest/spy@^2.1.0-beta.1": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.2.tgz#bccdeca597c8fc3777302889e8c98cec9264df44" + integrity sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A== + dependencies: + tinyspy "^3.0.0" + +"@vitest/utils@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.2.tgz#222ac35ba02493173e40581256eb7a62520fcdba" + integrity sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ== + dependencies: + "@vitest/pretty-format" "2.1.2" + loupe "^3.1.1" + tinyrainbow "^1.2.0" "@xterm/addon-canvas@^0.6.0-beta.1": version "0.6.0-beta.1" @@ -2458,18 +2567,6 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn-walk@^8.3.2: - version "8.3.3" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" - integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== - dependencies: - acorn "^8.11.0" - -acorn@^8.11.0, acorn@^8.11.3: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== - acorn@^8.8.2, acorn@^8.9.0: version "8.11.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" @@ -2497,6 +2594,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -2504,7 +2606,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -2516,6 +2618,11 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -2671,10 +2778,10 @@ assert@^2.0.0: object.assign "^4.1.4" util "^0.12.5" -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== ast-types-flow@^0.0.7: version "0.0.7" @@ -3020,18 +3127,16 @@ caniuse-lite@^1.0.30001580: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz#0dfd4db9e94edbdca67d57348ebc070dece279f4" integrity sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ== -chai@^4.3.10: - version "4.4.1" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" - integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== +chai@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.1.tgz#f035d9792a22b481ead1c65908d14bb62ec1c82c" + integrity sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA== dependencies: - assertion-error "^1.1.0" - check-error "^1.0.3" - deep-eql "^4.1.3" - get-func-name "^2.0.2" - loupe "^2.3.6" - pathval "^1.1.1" - type-detect "^4.0.8" + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" @@ -3063,12 +3168,10 @@ charcodes@^0.2.0: resolved "https://registry.yarnpkg.com/charcodes/-/charcodes-0.2.0.tgz#5208d327e6cc05f99eb80ffc814707572d1f14e4" integrity sha512-Y4kiDb+AM4Ecy58YkuZrrSRJBDQdQ2L+NyS1vHHFtNtUjgutcZfx3yp1dAONI/oPaPmyGfCLx5CxL+zauIMyKQ== -check-error@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" - integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== - dependencies: - get-func-name "^2.0.2" +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== chokidar@^3.5.3: version "3.5.3" @@ -3161,11 +3264,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -confbox@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" - integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== - confusing-browser-globals@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" @@ -3296,7 +3394,7 @@ create-require@^1.1.1: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -3378,7 +3476,7 @@ date-fns@^3.6.0: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== -debug@4: +debug@4, debug@^4.3.6: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -3418,12 +3516,10 @@ decimal.js@^10.4.3: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== -deep-eql@^4.1.3: - version "4.1.4" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" - integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== - dependencies: - type-detect "^4.0.0" +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== deep-is@^0.1.3: version "0.1.4" @@ -3478,11 +3574,6 @@ diff-sequences@^27.5.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.0.tgz#a8ac0cb742b17d6f30a6c43e233893a2402c0729" integrity sha512-ZsOBWnhXiH+Zn0DcBNX/tiQsqrREHs/6oQsEVy2VJJjrTblykPima11pyHMSA/7PGmD+fwclTnKVKL/qtNREDQ== -diff-sequences@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" - integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== - diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3576,6 +3667,11 @@ dotenv@^16.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.1.tgz#1d9931f1d3e5d2959350d1250efab299561f7f11" integrity sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + ejs@^3.1.6: version "3.1.10" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" @@ -3606,6 +3702,11 @@ elliptic@^6.5.3, elliptic@^6.5.4: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emoji-regex@^9.2.2: version "9.2.2" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" @@ -4219,21 +4320,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -execa@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" - integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^8.0.1" - human-signals "^5.0.0" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^4.1.0" - strip-final-newline "^3.0.0" - fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -4354,6 +4440,14 @@ foreach@^2.0.5: resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k= +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" @@ -4426,7 +4520,7 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-func-name@^2.0.1, get-func-name@^2.0.2: +get-func-name@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== @@ -4459,11 +4553,6 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" -get-stream@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" - integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== - get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -4493,6 +4582,18 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob@^10.4.1: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^7.1.3: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" @@ -4692,6 +4793,11 @@ html-encoding-sniffer@^4.0.0: dependencies: whatwg-encoding "^3.1.1" +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + html-minifier-terser@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" @@ -4726,11 +4832,6 @@ https-proxy-agent@^7.0.5: agent-base "^7.0.2" debug "4" -human-signals@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" - integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== - iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -4918,6 +5019,11 @@ is-finalizationregistry@^1.0.2: dependencies: call-bind "^1.0.2" +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-generator-function@^1.0.10, is-generator-function@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" @@ -4997,11 +5103,6 @@ is-shared-array-buffer@^1.0.2: dependencies: call-bind "^1.0.2" -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== - is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -5074,6 +5175,37 @@ isomorphic-timers-promises@^1.0.1: resolved "https://registry.yarnpkg.com/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz#e4137c24dbc54892de8abae3a4b5c1ffff381598" integrity sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ== +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz#acaef948df7747c8eb5fbf1265cb980f6353a441" + integrity sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== + dependencies: + "@jridgewell/trace-mapping" "^0.3.23" + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + +istanbul-reports@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -5085,6 +5217,15 @@ iterator.prototype@^1.1.2: reflect.getprototypeof "^1.0.4" set-function-name "^2.0.1" +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jake@^10.8.5: version "10.8.7" resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" @@ -5115,11 +5256,6 @@ jest-get-type@^27.5.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-tokens@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-9.0.0.tgz#0f893996d6f3ed46df7f0a3b12a03f5fd84223c1" - integrity sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -5240,14 +5376,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -local-pkg@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" - integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== - dependencies: - mlly "^1.4.2" - pkg-types "^1.0.3" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -5290,10 +5418,10 @@ loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4 dependencies: js-tokens "^3.0.0 || ^4.0.0" -loupe@^2.3.6, loupe@^2.3.7: - version "2.3.7" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" - integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== +loupe@^3.1.0, loupe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.1.tgz#71d038d59007d890e3247c5db97c1ec5a92edc54" + integrity sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw== dependencies: get-func-name "^2.0.1" @@ -5304,6 +5432,11 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -5323,6 +5456,13 @@ lz-string@^1.4.4: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= +magic-string@^0.30.11: + version "0.30.11" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954" + integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + magic-string@^0.30.3: version "0.30.5" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" @@ -5330,12 +5470,21 @@ magic-string@^0.30.3: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" -magic-string@^0.30.5: - version "0.30.10" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" - integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== +magicast@^0.3.4: + version "0.3.5" + resolved "https://registry.yarnpkg.com/magicast/-/magicast-0.3.5.tgz#8301c3c7d66704a0771eb1bad74274f0ec036739" + integrity sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ== dependencies: - "@jridgewell/sourcemap-codec" "^1.4.15" + "@babel/parser" "^7.25.4" + "@babel/types" "^7.25.4" + source-map-js "^1.2.0" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" md5.js@^1.3.4: version "1.3.5" @@ -5346,11 +5495,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -5384,11 +5528,6 @@ mime-types@^2.1.12: dependencies: mime-db "1.51.0" -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - min-indent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256" @@ -5450,15 +5589,10 @@ minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -mlly@^1.4.2, mlly@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" - integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== - dependencies: - acorn "^8.11.3" - pathe "^1.1.2" - pkg-types "^1.1.1" - ufo "^1.5.3" +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== monaco-editor@^0.45.0: version "0.45.0" @@ -5559,13 +5693,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^5.1.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" - integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== - dependencies: - path-key "^4.0.0" - nth-check@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" @@ -5718,13 +5845,6 @@ once@^1.3.0: dependencies: wrappy "1" -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -5756,13 +5876,6 @@ p-limit@^3.0.2: dependencies: yocto-queue "^0.1.0" -p-limit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-5.0.0.tgz#6946d5b7140b649b7a33a027d89b4c625b3a5985" - integrity sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== - dependencies: - yocto-queue "^1.0.0" - p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -5782,6 +5895,11 @@ p-try@^1.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + pako@~1.0.5: version "1.0.11" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -5863,16 +5981,19 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-key@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" - integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== - path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.9.0.tgz#5dc0753acbf8521ca2e0f137b4578b917b10cf24" @@ -5890,15 +6011,15 @@ pathe@^0.2.0: resolved "https://registry.yarnpkg.com/pathe/-/pathe-0.2.0.tgz#30fd7bbe0a0d91f0e60bae621f5d19e9e225c339" integrity sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw== -pathe@^1.1.1, pathe@^1.1.2: +pathe@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== pbkdf2@^3.0.3: version "3.1.2" @@ -5933,15 +6054,6 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -pkg-types@^1.0.3, pkg-types@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.3.tgz#161bb1242b21daf7795036803f28e30222e476e3" - integrity sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA== - dependencies: - confbox "^0.1.7" - mlly "^1.7.1" - pathe "^1.1.2" - postcss@^8.4.43: version "8.4.47" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" @@ -5977,15 +6089,6 @@ pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.0: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-format@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" - integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -6107,11 +6210,6 @@ react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - react-redux@^7.2.6: version "7.2.6" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.6.tgz#49633a24fe552b5f9caf58feb8a138936ddfe9aa" @@ -6531,7 +6629,7 @@ siginfo@^2.0.0: resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== -signal-exit@^4.1.0: +signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== @@ -6549,7 +6647,7 @@ snake-case@^3.0.4: dot-case "^3.0.4" tslib "^2.0.3" -source-map-js@^1.2.1: +source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -6582,7 +6680,7 @@ state-local@^1.0.6: resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5" integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w== -std-env@^3.5.0: +std-env@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== @@ -6610,6 +6708,33 @@ string-natural-compare@^3.0.1: resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + string.prototype.matchall@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa" @@ -6689,23 +6814,32 @@ string_decoder@^1.0.0, string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -6718,13 +6852,6 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-literal@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-2.1.0.tgz#6d82ade5e2e74f5c7e8739b6c84692bd65f0bd2a" - integrity sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw== - dependencies: - js-tokens "^9.0.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -6777,6 +6904,15 @@ terser@^5.10.0: commander "^2.20.0" source-map-support "~0.5.20" +test-exclude@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.1.tgz#20b3ba4906ac20994e275bbcafd68d510264c2a2" + integrity sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^10.4.1" + minimatch "^9.0.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" @@ -6799,20 +6935,30 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== -tinybench@^2.5.1: - version "2.8.0" - resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.8.0.tgz#30e19ae3a27508ee18273ffed9ac7018949acd7b" - integrity sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== -tinypool@^0.8.3: - version "0.8.4" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.4.tgz#e217fe1270d941b39e98c625dcecebb1408c9aa8" - integrity sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ== +tinyexec@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.0.tgz#ed60cfce19c17799d4a241e06b31b0ec2bee69e6" + integrity sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg== -tinyspy@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.1.tgz#117b2342f1f38a0dbdcc73a50a454883adf861d1" - integrity sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A== +tinypool@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe" + integrity sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA== + +tinyrainbow@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== + +tinyspy@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== tldts-core@^6.1.49: version "6.1.49" @@ -6926,11 +7072,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@^4.0.0, type-detect@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" @@ -6980,11 +7121,6 @@ typescript@^5.3.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== -ufo@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" - integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== - unbox-primitive@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" @@ -7094,15 +7230,14 @@ value-equal@^1.0.1: resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== -vite-node@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" - integrity sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw== +vite-node@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.2.tgz#f5491a2b399959c9e2f3c4b70cb0cbaecf9be6d2" + integrity sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ== dependencies: cac "^6.7.14" - debug "^4.3.4" - pathe "^1.1.1" - picocolors "^1.0.0" + debug "^4.3.6" + pathe "^1.1.2" vite "^5.0.0" vite-plugin-html@^3.2.2: @@ -7159,7 +7294,18 @@ vite-tsconfig-paths@^4.3.1: globrex "^0.1.2" tsconfck "^3.0.1" -vite@^5.0.0, vite@^5.2.14: +vite@^5.0.0: + version "5.4.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.8.tgz#af548ce1c211b2785478d3ba3e8da51e39a287e8" + integrity sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vite@^5.2.14: version "5.4.6" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.6.tgz#85a93a1228a7fb5a723ca1743e337a2588ed008f" integrity sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q== @@ -7170,31 +7316,30 @@ vite@^5.0.0, vite@^5.2.14: optionalDependencies: fsevents "~2.3.3" -vitest@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.6.0.tgz#9d5ad4752a3c451be919e412c597126cffb9892f" - integrity sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA== - dependencies: - "@vitest/expect" "1.6.0" - "@vitest/runner" "1.6.0" - "@vitest/snapshot" "1.6.0" - "@vitest/spy" "1.6.0" - "@vitest/utils" "1.6.0" - acorn-walk "^8.3.2" - chai "^4.3.10" - debug "^4.3.4" - execa "^8.0.1" - local-pkg "^0.5.0" - magic-string "^0.30.5" - pathe "^1.1.1" - picocolors "^1.0.0" - std-env "^3.5.0" - strip-literal "^2.0.0" - tinybench "^2.5.1" - tinypool "^0.8.3" +vitest@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.2.tgz#f285fdde876749fddc0cb4d9748ae224443c1694" + integrity sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A== + dependencies: + "@vitest/expect" "2.1.2" + "@vitest/mocker" "2.1.2" + "@vitest/pretty-format" "^2.1.2" + "@vitest/runner" "2.1.2" + "@vitest/snapshot" "2.1.2" + "@vitest/spy" "2.1.2" + "@vitest/utils" "2.1.2" + chai "^5.1.1" + debug "^4.3.6" + magic-string "^0.30.11" + pathe "^1.1.2" + std-env "^3.7.0" + tinybench "^2.9.0" + tinyexec "^0.3.0" + tinypool "^1.0.0" + tinyrainbow "^1.2.0" vite "^5.0.0" - vite-node "1.6.0" - why-is-node-running "^2.2.2" + vite-node "2.1.2" + why-is-node-running "^2.3.0" vm-browserify@^1.0.1: version "1.1.2" @@ -7307,10 +7452,10 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -why-is-node-running@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" - integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== dependencies: siginfo "^2.0.0" stackback "0.0.2" @@ -7405,6 +7550,24 @@ workbox-streams@^6.5.4: workbox-core "6.6.1" workbox-routing "6.6.1" +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -7449,8 +7612,3 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -yocto-queue@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.1.1.tgz#fef65ce3ac9f8a32ceac5a634f74e17e5b232110" - integrity sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g== From 9558ed0d6552216124dfd91da1843d63741db558 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 21:40:55 -0400 Subject: [PATCH 32/43] fix: fix start line number --- .../CodeEditor/autocomplete/parse.test.ts | 24 ++++++++++++++++++- .../CodeEditor/autocomplete/parse.ts | 3 ++- .../autocomplete/testdata/hello.txt | 9 +++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/testdata/hello.txt diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts index e3b770f5..2b1ec350 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.test.ts @@ -37,6 +37,28 @@ describe('buildImportContest', () => { monaco.languages.register({ id: 'go' }) monaco.languages.setMonarchTokensProvider('go', language) + test('should generate correct ranges', async () => { + await runContextTest({ + sourceFile: 'hello.txt', + want: { + hasError: false, + allPaths: new Set(['fmt']), + blockPaths: ['fmt'], + blockType: ImportClauseType.Block, + range: { + startLineNumber: 3, + startColumn: 1, + endLineNumber: 5, + endColumn: 2, + }, + totalRange: { + startLineNumber: 1, + endLineNumber: 5, + }, + }, + }) + }) + test('should support inline import', async () => { await runContextTest({ sourceFile: 'single.txt', @@ -68,7 +90,7 @@ describe('buildImportContest', () => { blockPaths: ['fmt', 'bar'], blockType: ImportClauseType.Block, range: { - startLineNumber: 2, + startLineNumber: 3, startColumn: 1, endLineNumber: 6, endColumn: 2, diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts index 6e8babb7..6a53a477 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts @@ -267,7 +267,7 @@ const traverseImportGroup = ( let groupStartFound = header.hasOpenParen ?? false const imports: ImportStmt[] = [] const range = { - startLineNumber: header.rowIndex, + startLineNumber: header.rowIndex + 1, startColumn: 1, endLineNumber: -1, endColumn: -1, @@ -422,6 +422,7 @@ export const importContextFromTokens = (model: monaco.editor.ITextModel, tokens: // TODO: support named imports return { hasError, + // prependNewLine: lastImportBlock.isMultiline, allPaths: new Set(allImports), blockPaths: lastImportBlock.imports.map(({ path }) => path), blockType: lastImportBlock.isMultiline ? ImportClauseType.Block : ImportClauseType.Single, diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/hello.txt b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/hello.txt new file mode 100644 index 00000000..2ca0d5e0 --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/testdata/hello.txt @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, World!") +} From fc684f3ffbf32d4459401de1482fd01264b57ebe Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 21:47:22 -0400 Subject: [PATCH 33/43] fix: add additional index --- .../features/workspace/CodeEditor/autocomplete/cache.ts | 7 +++++-- web/src/services/storage/db.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts index 0c8c8144..04cef8e6 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts @@ -2,6 +2,8 @@ import type * as monaco from 'monaco-editor' import type { ImportsContext } from '~/services/completion' import { buildImportContext } from './parse' +const stripSlash = (str: string) => str[0] == '/' ? str.slice(1) : str + /** * Stores document metadata (such as symbols, imports) in cache. */ @@ -53,9 +55,11 @@ export class DocumentMetadataCache { * Populates data from model if it's not cached. */ getMetadata(fileName: string, model: monaco.editor.ITextModel) { + // model file name has slash at root + fileName = stripSlash(fileName) + const data = this.cache.get(fileName) if (data && !data.hasError) { - console.log('use cache', data) return data } @@ -64,7 +68,6 @@ export class DocumentMetadataCache { private updateCache(fileName: string, model: monaco.editor.ITextModel) { const context = buildImportContext(model) - console.log('cache empty, update meta', context) this.cache.set(fileName, context) return context } diff --git a/web/src/services/storage/db.ts b/web/src/services/storage/db.ts index 7a304eee..e417700d 100644 --- a/web/src/services/storage/db.ts +++ b/web/src/services/storage/db.ts @@ -15,7 +15,7 @@ export class DatabaseStorage extends Dexie { this.version(2).stores({ keyValue: 'key', packageIndex: 'importPath, prefix, name', - symbolIndex: 'key, packagePath, [prefix+packageName], [packageName+label]', + symbolIndex: 'key, packagePath, [packageName+prefix], [packageName+label], [packagePath+prefix]', }) } } From acb0aabf3d2f5a42c3c691a7c2293cc3cd06ed91 Mon Sep 17 00:00:00 2001 From: x1unix Date: Wed, 2 Oct 2024 21:53:20 -0400 Subject: [PATCH 34/43] chore: use built-in cache in setup-go --- .github/workflows/pull_request.yml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 30efea17..ef5fbb2a 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,21 +12,6 @@ jobs: test: runs-on: ubuntu-latest steps: - - id: go-cache-paths - run: | - echo "::set-output name=go-build::$(go env GOCACHE)" - echo "::set-output name=go-mod::$(go env GOMODCACHE)" - - uses: actions/checkout@v4 - - name: Go Build Cache - uses: actions/cache@v4 - with: - path: ${{ steps.go-cache-paths.outputs.go-build }} - key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} - - name: Go Mod Cache - uses: actions/cache@v4 - with: - path: ${{ steps.go-cache-paths.outputs.go-mod }} - key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} - uses: actions/cache@v4 env: cache-name: npm-cache @@ -47,7 +32,8 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: "^1.21.0" + go-version: "^1.23.0" + cache-dependency-path: go.sum - name: golangci-lint uses: golangci/golangci-lint-action@v6.1.0 with: From d140174740dfdf8a943e8b8b7f426330c2cb955e Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 00:17:14 -0400 Subject: [PATCH 35/43] feat: show completion after dot --- .../autocomplete/symbols/parse.test.ts | 43 +++++++++++++++++++ .../CodeEditor/autocomplete/symbols/parse.ts | 28 ++++++------ .../autocomplete/symbols/provider.ts | 23 ++++++---- web/src/services/completion/service.ts | 22 +++++----- .../services/completion/types/suggestion.ts | 11 ++++- web/src/services/completion/utils.ts | 8 +++- 6 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 web/src/components/features/workspace/CodeEditor/autocomplete/symbols/parse.test.ts diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/parse.test.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/parse.test.ts new file mode 100644 index 00000000..99052c68 --- /dev/null +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/parse.test.ts @@ -0,0 +1,43 @@ +import { assert, describe, test } from 'vitest' +import { parseExpression } from './parse' + +const testParseExpression = (input: string, want: ReturnType) => { + const got = parseExpression(input) + assert.deepEqual(got, want) +} + +describe('parseQuery', () => { + test('should return null for empty values', () => { + testParseExpression('', null) + }) + + test('should parse literals', () => { + testParseExpression('foo', { + value: 'foo', + }) + }) + + test('should return package query for dot', () => { + testParseExpression('foo.', { + packageName: 'foo', + }) + }) + + test('should return package symbol query', () => { + testParseExpression('fmt.Println', { + packageName: 'fmt', + value: 'Println', + }) + }) + + test('should ignore spaces before string', () => { + testParseExpression(' os.Environ', { + packageName: 'os', + value: 'Environ', + }) + }) + + test('should omit unmatched long expressions', () => { + testParseExpression('foo.bar.baz', null) + }) +}) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/parse.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/parse.ts index e063875e..ea476fa9 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/parse.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/parse.ts @@ -1,24 +1,28 @@ // Matches package (and method name) -const COMPL_REGEXP = /([a-zA-Z0-9_]+)(\.([A-Za-z0-9_]+))?$/ -const R_GROUP_PKG = 1 -const R_GROUP_METHOD = 3 +const queryRegexp = /(^|\s)([a-z0-9_]+)(\.([a-z0-9_]+)?)?$/i export const parseExpression = (expr: string) => { - COMPL_REGEXP.lastIndex = 0 // Reset regex state - const m = COMPL_REGEXP.exec(expr) - if (!m) { + queryRegexp.lastIndex = 0 // Reset regex state + const match = queryRegexp.exec(expr) + if (!match) { return null } - const varName = m[R_GROUP_PKG] - const propValue = m[R_GROUP_METHOD] + const [, , lhv, delim, rhv] = match + if (rhv) { + return { + packageName: lhv, + value: rhv, + } + } - if (!propValue) { - return { value: varName } + if (delim) { + return { + packageName: lhv, + } } return { - packageName: varName, - value: propValue, + value: lhv, } } diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts index a258be26..97675536 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/symbols/provider.ts @@ -13,6 +13,7 @@ const SUGGESTIONS_DEBOUNCE_DELAY = 500 * Provides completion for symbols such as variables and functions. */ export class GoSymbolsCompletionItemProvider extends CacheBasedCompletionProvider { + triggerCharacters = Array.from('.abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') private readonly metadataCache: DocumentMetadataCache private readonly getSuggestionFunc = asyncDebounce( async (query) => await this.completionSvc.getSymbolSuggestions(query), @@ -24,12 +25,19 @@ export class GoSymbolsCompletionItemProvider extends CacheBasedCompletionProvide this.metadataCache = metadataCache } - protected getFallbackSuggestions({ value, context: { range } }: SuggestionQuery): monaco.languages.CompletionList { + protected getFallbackSuggestions(query: SuggestionQuery): monaco.languages.CompletionList { + if ('packageName' in query) { + return { suggestions: [] } + } + // filter snippets by prefix. // usually monaco does that but not always in right way - const suggestions = snippets.filter((s) => s.label.startsWith(value)).map((s) => ({ ...s, range })) + const { value } = query + const items = value ? snippets.filter((s) => s.label.startsWith(value)) : snippets - return { suggestions } + return { + suggestions: items, + } } protected parseCompletionQuery( @@ -71,14 +79,11 @@ export class GoSymbolsCompletionItemProvider extends CacheBasedCompletionProvide protected async querySuggestions(query: SuggestionQuery) { const { suggestions: relatedSnippets } = this.getFallbackSuggestions(query) - const suggestions = await this.getSuggestionFunc(query) - if (!suggestions?.length) { + const results = await this.getSuggestionFunc(query) + if (!results?.length) { return relatedSnippets } - const { - context: { range }, - } = query - return relatedSnippets.concat(suggestions.map((s) => ({ ...s, range }))) + return relatedSnippets.length ? relatedSnippets.concat(results) : results } } diff --git a/web/src/services/completion/service.ts b/web/src/services/completion/service.ts index 7d1a2bfc..30f05405 100644 --- a/web/src/services/completion/service.ts +++ b/web/src/services/completion/service.ts @@ -1,5 +1,5 @@ import { db, keyValue } from '../storage' -import type { GoIndexFile, SuggestionQuery } from './types' +import type { GoIndexFile, LiteralQuery, PackageSymbolQuery, SuggestionQuery } from './types' import { completionFromPackage, completionFromSymbol, @@ -53,31 +53,33 @@ export class GoCompletionService { async getSymbolSuggestions(query: SuggestionQuery) { await this.checkCacheReady() - if (query.packageName) { - return await this.getMemberSuggestion(query as Required) + if ('packageName' in query) { + return await this.getMemberSuggestion(query) } return await this.getLiteralSuggestion(query) } - private async getMemberSuggestion({ value, packageName, context }: Required) { + private async getMemberSuggestion({ value, packageName, context }: PackageSymbolQuery) { // If package with specified name is imported - filter symbols // to avoid overlap with packages with eponymous name. const packagePath = findPackagePathFromContext(context, packageName) - const prefix = value.toLowerCase() - const query: Partial = packagePath + const filter: Partial = packagePath ? { packagePath, - prefix, } - : { packageName, prefix } + : { packageName } - const symbols = await this.db.symbolIndex.where(query).toArray() + if (value) { + filter.prefix = value.charAt(0).toLowerCase() + } + + const symbols = await this.db.symbolIndex.where(filter).toArray() return symbols.map((symbol) => completionFromSymbol(symbol, context, !!packagePath)) } - private async getLiteralSuggestion({ value, context }: SuggestionQuery) { + private async getLiteralSuggestion({ value, context }: LiteralQuery) { const packages = await this.db.packageIndex.where('prefix').equals(value).toArray() const builtins = await this.db.symbolIndex.where('packagePath').equals('builtin').toArray() diff --git a/web/src/services/completion/types/suggestion.ts b/web/src/services/completion/types/suggestion.ts index 89cb93c7..19672dd7 100644 --- a/web/src/services/completion/types/suggestion.ts +++ b/web/src/services/completion/types/suggestion.ts @@ -80,8 +80,15 @@ export interface SuggestionContext { imports: ImportsContext } -export interface SuggestionQuery { - packageName?: string +export interface LiteralQuery { value: string context: SuggestionContext } + +export interface PackageSymbolQuery { + packageName: string + value?: string + context: SuggestionContext +} + +export type SuggestionQuery = LiteralQuery | PackageSymbolQuery diff --git a/web/src/services/completion/utils.ts b/web/src/services/completion/utils.ts index 60ade8df..86db0eee 100644 --- a/web/src/services/completion/utils.ts +++ b/web/src/services/completion/utils.ts @@ -12,7 +12,7 @@ const stubRange = undefined as any as monaco.IRange const packageCompletionKind = 8 -const fallbackValue = (str: string, defaults?: string | undefined) => (str.length ? str : defaults) +const discardIfEmpty = (str: string, defaults?: string | undefined) => (str.length ? str : defaults) const stringToMarkdown = (value: string): monaco.IMarkdownString | undefined => { if (!value.length) { @@ -46,7 +46,7 @@ export const constructSymbols = ({ names.map((name, i) => ({ key: `${packages[i][SymbolSourceKey.Path]}.${name}`, label: name, - detail: fallbackValue(details[i], name), + detail: discardIfEmpty(details[i], name), signature: signatures[i], kind: kinds[i], insertText: insertTexts[i], @@ -140,6 +140,10 @@ export const findPackagePathFromContext = ({ imports }: SuggestionContext, pkgNa return undefined } + if (imports.allPaths.has(pkgName)) { + return pkgName + } + for (const importPath of imports.allPaths.keys()) { // TODO: support named imports if (pkgName === pkgNameFromPath(importPath)) { From 63f60d0d339460e05de98608ec244c7b7126aabc Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 00:57:33 -0400 Subject: [PATCH 36/43] fix: fix PR job --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ef5fbb2a..8d800409 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,6 +12,7 @@ jobs: test: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - uses: actions/cache@v4 env: cache-name: npm-cache From 8dc31fd40e6beed55dc6449dc9a38e770c8689b7 Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 00:59:54 -0400 Subject: [PATCH 37/43] fix: linter --- .../features/workspace/CodeEditor/autocomplete/cache.ts | 2 +- .../features/workspace/CodeEditor/autocomplete/parse.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts index 04cef8e6..e935d35c 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/cache.ts @@ -2,7 +2,7 @@ import type * as monaco from 'monaco-editor' import type { ImportsContext } from '~/services/completion' import { buildImportContext } from './parse' -const stripSlash = (str: string) => str[0] == '/' ? str.slice(1) : str +const stripSlash = (str: string) => (str[0] === '/' ? str.slice(1) : str) /** * Stores document metadata (such as symbols, imports) in cache. diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts index 6a53a477..5d0f0a8a 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts @@ -210,7 +210,7 @@ const readImportBlockLine = (line: number, model: monaco.editor.ITextModel, row: const params: ReadTokenParams = { line, model, - tokens: slice + tokens: slice, } slice = slice.slice(i + 1) const { isParen, isClose, value } = checkParenthesis(i, params) From 84ad5cfb2d2fa4d31fc3badf38fad845427b5e0e Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 01:00:12 -0400 Subject: [PATCH 38/43] fix: linter --- .../features/workspace/CodeEditor/autocomplete/parse.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts index 5d0f0a8a..6c5d0d60 100644 --- a/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts +++ b/web/src/components/features/workspace/CodeEditor/autocomplete/parse.ts @@ -167,9 +167,9 @@ const readImportLine = (line: number, model: monaco.editor.ITextModel, row: mona const i = row.findIndex(isNotEmptyToken) const token = row[i] const params: ReadTokenParams = { - line: line, + line, + model, tokens: row, - model: model, } switch (token.type) { case GoToken.Ident: { From 6b6d86c5224bbef782da41ee225fee8a057b5e87 Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 01:04:40 -0400 Subject: [PATCH 39/43] fix: linter --- internal/pkgindex/index/scanner.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/internal/pkgindex/index/scanner.go b/internal/pkgindex/index/scanner.go index fc77ea9d..faaee593 100644 --- a/internal/pkgindex/index/scanner.go +++ b/internal/pkgindex/index/scanner.go @@ -32,13 +32,6 @@ type scanEntry struct { importPath string } -func (ent scanEntry) makeChild(dirName string) scanEntry { - return scanEntry{ - path: filepath.Join(ent.path, dirName), - importPath: path.Join(ent.importPath, dirName), - } -} - func ScanRoot(goRoot string) (*GoIndexFile, error) { goVersion, err := imports.CheckVersionFile(goRoot) if err != nil { @@ -124,7 +117,7 @@ func enqueueRootEntries(rootDir string, parentImportPath string, queue *imports. } queue.Add(scanEntry{ path: absPath, - importPath: entry.Name(), + importPath: importPath, }) } From 70027004d90fc36fa5c30dd9d6ae0102ea25ef7c Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 01:05:04 -0400 Subject: [PATCH 40/43] chore: bump golangci-lint --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 8d800409..ada22460 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -38,6 +38,6 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v6.1.0 with: - version: v1.54.2 + version: v1.61.1 - run: go version - run: go test ./... From 18df241c1adddb9189760c96de4a7a399d97090f Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 01:07:26 -0400 Subject: [PATCH 41/43] chore: bump golangci-lint --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index ada22460..4233820f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -38,6 +38,6 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v6.1.0 with: - version: v1.61.1 + version: v1.61.0 - run: go version - run: go test ./... From eaefda731518c384a1aacf7a58841b1eed052956 Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 01:11:37 -0400 Subject: [PATCH 42/43] chore: use node cache --- .github/workflows/pull_request.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4233820f..521736c6 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -21,6 +21,12 @@ jobs: key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- + - name: Setup node + uses: actions/setup-node@v4 + with: + cache: 'yarn' + node-version-file: '.nvmrc' + cache-dependency-path: 'web/yarn.lock' - name: Install modules run: yarn working-directory: ./web From 809098ebbe6f2ae6c5e86843e636284a9a38074e Mon Sep 17 00:00:00 2001 From: x1unix Date: Thu, 3 Oct 2024 01:13:00 -0400 Subject: [PATCH 43/43] fix: tests --- internal/pkgindex/imports/parser_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/pkgindex/imports/parser_test.go b/internal/pkgindex/imports/parser_test.go index db98f6d0..0ba35988 100644 --- a/internal/pkgindex/imports/parser_test.go +++ b/internal/pkgindex/imports/parser_test.go @@ -12,6 +12,8 @@ import ( "github.com/x1unix/go-playground/pkg/monaco" ) +const goDocDomain = "pkg.go.dev" + func TestParseImportCompletionItem(t *testing.T) { cases := map[string]struct { pkgName string