diff --git a/src/codeWalkerService.ts b/src/codeWalkerService.ts index d4e8fac..0500933 100644 --- a/src/codeWalkerService.ts +++ b/src/codeWalkerService.ts @@ -3,6 +3,7 @@ import { URI } from 'vscode-uri'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { ContractCollection } from './model/contractsCollection'; import { Parser } from './parser/tact'; +import { DocumentStore } from './documentStore'; export class ParsedCode { public element: any; @@ -370,17 +371,19 @@ export class DocumentContract { export class TactCodeWalker { private rootPath: string | undefined; private tactparser = new Parser(); + private documentStore: DocumentStore; - constructor(rootPath: string | undefined) { + constructor(rootPath: string | undefined, documentStore: DocumentStore) { this.rootPath = rootPath; + this.documentStore = documentStore; } - public getAllContracts(document: TextDocument, position: vscode.Position): DocumentContract { + public async getAllContracts(document: TextDocument, position: vscode.Position): Promise { let documentContract:DocumentContract = new DocumentContract(); const documentText = document.getText(); const contractPath = URI.parse(document.uri).fsPath; - const contracts = new ContractCollection(); - contracts.addContractAndResolveImports( + const contracts = new ContractCollection(this.documentStore); + await contracts.addContractAndResolveImports( contractPath, documentText ); diff --git a/src/completionService.ts b/src/completionService.ts index 5cddeb7..fa1b625 100644 --- a/src/completionService.ts +++ b/src/completionService.ts @@ -7,13 +7,16 @@ import { CompletionItem, CompletionItemKind } from 'vscode-languageserver'; import * as vscode from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import {Contract2, DeclarationType, DocumentContract, Function, TactCodeWalker, Variable, Struct, Message} from './codeWalkerService'; +import { DocumentStore } from './documentStore'; export class CompletionService { public rootPath: string | undefined; + private documentStore: DocumentStore; - constructor(rootPath: string | undefined) { + constructor(rootPath: string | undefined, documentStore: DocumentStore) { this.rootPath = rootPath; + this.documentStore = documentStore; } public getTypeString(literal: any) { @@ -157,9 +160,9 @@ export class CompletionService { return completionItem; } - public getAllCompletionItems( + public async getAllCompletionItems( document: TextDocument | undefined, - position: vscode.Position ): CompletionItem[] { + position: vscode.Position ): Promise { if (document == undefined) { return []; } @@ -167,10 +170,10 @@ export class CompletionService { let triggeredByImport = false; let triggeredByDotStart = 0; try { - var walker = new TactCodeWalker(this.rootPath); + var walker = new TactCodeWalker(this.rootPath, this.documentStore); const offset = document.offsetAt(position); - var documentContractSelected = walker.getAllContracts(document, position); + var documentContractSelected = await walker.getAllContracts(document, position); const lines = document.getText().split(/\r?\n/g); triggeredByDotStart = this.getTriggeredByDotStart(lines, position); @@ -1602,4 +1605,4 @@ function getSliceCompletionItems(): CompletionItem[] { label: 'asString', }, ] -} \ No newline at end of file +} diff --git a/src/definitionProvider.ts b/src/definitionProvider.ts index 81390f3..8c869a0 100644 --- a/src/definitionProvider.ts +++ b/src/definitionProvider.ts @@ -6,12 +6,15 @@ import { TextDocument } from 'vscode-languageserver-textdocument'; import { Contract } from './model/contract'; import { ContractCollection } from './model/contractsCollection'; import { Parser } from './parser/tact'; +import { DocumentStore } from './documentStore'; const tactparse = new Parser(); export class TactDefinitionProvider { private rootPath: string | undefined; + private documentStore: DocumentStore; - constructor(rootPath: string | undefined) { + constructor(rootPath: string | undefined, documentStore: DocumentStore) { this.rootPath = rootPath; + this.documentStore = documentStore; } /** @@ -24,15 +27,14 @@ export class TactDefinitionProvider { * @returns {(Thenable)} * @memberof TactDefinitionProvider */ - public provideDefinition( + public async provideDefinition( document: TextDocument, position: vscode.Position, - ): Thenable | undefined { + ): Promise { const documentText = document.getText(); const contractPath = URI.parse(document.uri).fsPath; - - const contracts = new ContractCollection(); - contracts.addContractAndResolveImports( + const contracts = new ContractCollection(this.documentStore); + await contracts.addContractAndResolveImports( contractPath, documentText ); diff --git a/src/documentStore.ts b/src/documentStore.ts new file mode 100644 index 0000000..7f0302e --- /dev/null +++ b/src/documentStore.ts @@ -0,0 +1,109 @@ +import * as lsp from 'vscode-languageserver'; +import { TextDocuments } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { LRUMap } from './util/lruMap'; + +export interface TextDocumentChange2 { + document: TextDocument, + changes: { + range: lsp.Range; + rangeOffset: number; + rangeLength: number; + text: string; + }[] +} + +type DocumentEntry = { exists: true, document: TextDocument } | { exists: false, document: undefined }; + +export class DocumentStore extends TextDocuments { + + private readonly _onDidChangeContent2 = new lsp.Emitter(); + readonly onDidChangeContent2 = this._onDidChangeContent2.event; + + private readonly _decoder = new TextDecoder(); + private readonly _fileDocuments: LRUMap>; + + constructor(private readonly _connection: lsp.Connection) { + super({ + create: TextDocument.create, + update: (doc, changes, version) => { + let result: TextDocument; + let incremental = true; + let event: TextDocumentChange2 = { document: doc, changes: [] }; + + for (const change of changes) { + if (!lsp.TextDocumentContentChangeEvent.isIncremental(change)) { + incremental = false; + break; + } + const rangeOffset = doc.offsetAt(change.range.start); + event.changes.push({ + text: change.text, + range: change.range, + rangeOffset, + rangeLength: change.rangeLength ?? doc.offsetAt(change.range.end) - rangeOffset, + }); + } + result = TextDocument.update(doc, changes, version); + if (incremental) { + this._onDidChangeContent2.fire(event); + } + return result; + } + }); + + this._fileDocuments = new LRUMap>({ + size: 200, + dispose: _entries => { } + }); + + super.listen(_connection); + } + + async retrieve(uri: string): Promise { + let result = this.get(uri); + if (result) { + return { exists: true, document: result }; + } + + let promise = this._fileDocuments.get(uri); + if (!promise) { + promise = this._requestDocument(uri); + this._fileDocuments.set(uri, promise); + } + return promise; + } + + private async _requestDocument(uri: string): Promise { + const localPath = uriToPath(uri); + // Check if the file exists locally + // if (localPath && fs.existsSync(localPath)) { + // const content = fs.readFileSync(localPath, { encoding: 'utf-8' }); + // const document = TextDocument.create(uri, "tact", 1, content); + // return document; + // } + const reply = await this._connection.sendRequest<{ type: string, content: String, message?: string}>('file/read', uri); + if(reply.type === 'error') { + return { exists: false, document: undefined }; + } + // I am getting buffer from server, so I need to convert it to string + // to create TextDocument + const content = Buffer.from(reply.content).toString('utf-8'); + const document = TextDocument.create(uri, "tact", 1, content); + return { exists: true, document: document }; + } + + // remove "file document", e.g one that has been retrieved from "disk" + // and not one that is sync'd by LSP + removeFile(uri: string) { + return this._fileDocuments.delete(uri); + } +} + + +function uriToPath(uri: string): string | null { + if (uri.startsWith('file://')) { + return uri.slice(7); // Remove 'file://' from URI to get the path + } + return null; +} diff --git a/src/extension.ts b/src/extension.ts index 0ec632e..a3a5a2f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,32 +6,12 @@ import { } from 'vscode-languageclient/node'; // tslint:disable-next-line:no-duplicate-imports -import { - workspace, ExtensionContext, DiagnosticCollection, - languages, TextDocument, - FormattingOptions, CancellationToken, - ProviderResult, TextEdit, - DocumentSelector -} from 'vscode'; - -import { formatDocument } from './formatter'; +import { workspace, ExtensionContext, Uri, window, FileSystemError } from 'vscode'; -let diagnosticCollection: DiagnosticCollection; let clientDisposable: LanguageClient; export async function activate(context: ExtensionContext) { const ws = workspace.workspaceFolders; - diagnosticCollection = languages.createDiagnosticCollection('tact'); - - context.subscriptions.push(diagnosticCollection); - - context.subscriptions.push( - languages.registerDocumentFormattingEditProvider('tact', { - provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken): ProviderResult { - return Promise.resolve(formatDocument(document, context)); - }, - }) - ); const serverModule = path.join(__dirname, './server.js'); @@ -73,6 +53,22 @@ export async function activate(context: ExtensionContext) { }); clientDisposable.start(); + + clientDisposable.onReady().then(() => { + clientDisposable.onRequest('file/read', async raw => { + const uri = Uri.parse(raw); + try { + const content = await workspace.fs.readFile(uri); + return { type: 'success', content: content.toString() }; + } catch (error) { + if (error instanceof FileSystemError) { + return { type: 'error', message: 'File not found'}; + } + console.warn(error); + return { type: 'error', message: 'Unknown error'}; + } + }); + }) } } @@ -81,4 +77,4 @@ export function deactivate(): Thenable | undefined { return undefined; } return clientDisposable.stop(); -} \ No newline at end of file +} diff --git a/src/formatter.ts b/src/formatter.ts index 9c7e132..7cdb423 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -1,40 +1,44 @@ import * as prettier from 'prettier'; -import { workspace, ExtensionContext, Range, TextDocument, TextEdit } from 'vscode'; import * as path from 'path'; +import { TextEdit, Range } from 'vscode-languageserver-types'; +import { TextDocument } from 'vscode-languageserver-textdocument'; -export function formatDocument(document: TextDocument, context: ExtensionContext): TextEdit[] | undefined { - const rootPath = workspace.getWorkspaceFolder(document.uri)?.name; - - const ignoreOptions = { ignorePath: path.join(rootPath ? rootPath: "", '.prettierignore') }; - - const fileInfo = prettier.getFileInfo.sync(document.uri.fsPath, ignoreOptions); - - if (!fileInfo.ignored) { - const source = document.getText(); - // @TODO create npm module later - const pluginPath = path.join(context.extensionPath, 'out', 'prettier-plugin-tact', 'prettier-plugin-tact.js'); // 'node_modules', - const options = { +export function formatDocument(document: TextDocument, rootPath: string): TextEdit[] | undefined { + + if (rootPath) { + const ignoreOptions = { ignorePath: path.join(rootPath, '.prettierignore') }; + const fileInfo = prettier.getFileInfo.sync(document.uri, ignoreOptions); + if (fileInfo.ignored) return; + } + + const lspPath = __dirname; + + const source = document.getText(); + // @TODO create npm module later + const pluginPath = path.join(lspPath, 'prettier-plugin-tact', 'prettier-plugin-tact.js'); // 'node_modules', + const options = { 'parser': 'tact-parser', - 'pluginSearchDirs': [context.extensionPath], + 'pluginSearchDirs': [lspPath], 'plugins': [pluginPath], - }; - // - const config = prettier.resolveConfig.sync(document.uri.fsPath); - if (config !== null) { + }; + + const config = prettier.resolveConfig.sync(document.uri); + + if (config !== null) { prettier.clearConfigCache(); - } - Object.assign(options, config); - - const firstLine = document.lineAt(0); - const lastLine = document.lineAt(document.lineCount - 1); - const fullTextRange = new Range(firstLine.range.start, lastLine.range.end); - let formatted = ""; - try { + } + Object.assign(options, config); + + const startIndex = document.positionAt(0); + const endIndex = document.positionAt(document.getText().length); + + const fullTextRange = Range.create(startIndex, endIndex); + let formatted = ""; + try { formatted = prettier.format(source, options); - } catch(e) { + } catch (e) { formatted = source; console.log(e); - } - return [TextEdit.replace(fullTextRange, formatted)]; } + return [TextEdit.replace(fullTextRange, formatted)]; } diff --git a/src/hoverService.ts b/src/hoverService.ts index 764a4b3..584385c 100644 --- a/src/hoverService.ts +++ b/src/hoverService.ts @@ -2,18 +2,21 @@ import { Position } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { TactCodeWalker, Variable } from './codeWalkerService'; -import { MarkupContent, MarkupKind } from 'vscode-languageserver/node'; +import { MarkupContent, MarkupKind } from 'vscode-languageserver'; +import { DocumentStore } from './documentStore'; export class HoverService { public rootPath: string | undefined; + private documentStore: DocumentStore; - constructor(rootPath: string | undefined) { + constructor(rootPath: string | undefined, documentStore: DocumentStore) { this.rootPath = rootPath; + this.documentStore = documentStore; } - public getHoverItems( document: TextDocument | undefined, - position: Position): MarkupContent { + public async getHoverItems( document: TextDocument | undefined, + position: Position): Promise { if (document == undefined) { return { kind: MarkupKind.Markdown, @@ -33,10 +36,10 @@ export class HoverService { }; } - var walker = new TactCodeWalker(this.rootPath); + var walker = new TactCodeWalker(this.rootPath, this.documentStore); const offset = document.offsetAt(position); - var documentContractSelected = walker.getAllContracts(document, position); + var documentContractSelected = await walker.getAllContracts(document, position); let variableType: string | undefined = "global"; if (documentContractSelected != undefined && documentContractSelected.selectedContract != undefined) { @@ -183,4 +186,4 @@ const hoverDescription = { "Return Cell." ] }, -} \ No newline at end of file +} diff --git a/src/model/contractsCollection.ts b/src/model/contractsCollection.ts index 953846c..e41858c 100644 --- a/src/model/contractsCollection.ts +++ b/src/model/contractsCollection.ts @@ -1,11 +1,14 @@ 'use strict'; import * as fs from 'fs'; import {Contract} from './contract'; +import { DocumentStore } from '../documentStore'; export class ContractCollection { public contracts: Array; - constructor() { + private documentStore: DocumentStore; + constructor(documentStore: DocumentStore) { this.contracts = new Array(); + this.documentStore = documentStore; } public findContract(contract: Contract, contractPath: string) { @@ -33,20 +36,25 @@ export class ContractCollection { return compilation; } - public addContractAndResolveImports(contractPath: string, code: string) { + public async addContractAndResolveImports(contractPath: string, code: string) { const contract = this.addContract(contractPath, code); if (contract !== null) { contract.resolveImports(); - contract.imports.forEach(foundImport => { + + // Collect all the promises from the imports + const importPromises = contract.imports.map(async foundImport => { if (fs.existsSync(foundImport)) { if (!this.containsContract(foundImport)) { - const importContractCode = this.readContractCode(foundImport); - if (importContractCode != null) { - this.addContractAndResolveImports(foundImport, importContractCode); + const importContractCode = await this.readContractCode(foundImport); + if (importContractCode.exists) { + await this.addContractAndResolveImports(foundImport, importContractCode.document.getText()); } } } }); + + // Wait for all the import promises to resolve + await Promise.all(importPromises); } return contract; } @@ -60,10 +68,7 @@ export class ContractCollection { return null; } - private readContractCode(contractPath: string) { - if (fs.existsSync(contractPath)) { - return fs.readFileSync(contractPath, 'utf8'); - } - return null; + private async readContractCode(contractPath: string) { + return await this.documentStore.retrieve(contractPath); } } diff --git a/src/refactorService.ts b/src/refactorService.ts index 85ae98f..7da3029 100644 --- a/src/refactorService.ts +++ b/src/refactorService.ts @@ -1,7 +1,7 @@ 'use strict'; import { Position } from 'vscode-languageserver'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { Range, WorkspaceChange } from 'vscode-languageserver/node'; +import { Range, WorkspaceChange } from 'vscode-languageserver'; export class RefactorService { @@ -53,8 +53,13 @@ export class RefactorService { const wordObject = this.getWord(wordLine, range.start.character-1, [" ", "(", ")", "[", "]", ";", ",", "!", "+", "-", "*", ":", "{", "=", "&", "^", "%", "~"]); + const startLineText = document.getText({ + start: { line: range.start.line, character: 0 }, + end: { line: range.start.line, character: range.start.character } + }) || ''; + const indentation = startLineText.match(/^\s*/)?.[0] || ''; const workspaceEdit = new WorkspaceChange(); - workspaceEdit.getTextEditChange(document.uri).insert(Position.create(range.start.line+1, 0), `dump(${wordObject.word});\n`); + workspaceEdit.getTextEditChange(document.uri).insert(Position.create(range.start.line + 1, 0), `${indentation}dump(${wordObject.word});\n`); return workspaceEdit.edit; } @@ -83,4 +88,4 @@ export class RefactorService { "word": word } } -} \ No newline at end of file +} diff --git a/src/server.ts b/src/server.ts index 27477d8..934d293 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,6 +16,8 @@ import { HoverService } from './hoverService'; import { RefactorService } from './refactorService'; import { TactCompiler } from './tactCompiler'; import { URI } from 'vscode-uri'; +import { formatDocument } from './formatter'; +import { DocumentStore } from './documentStore'; interface Settings { tact: TactSettings; @@ -35,12 +37,13 @@ const connection: Connection = createConnection(ProposedFeatures.all); console.log = connection.console.log.bind(connection.console); console.error = connection.console.error.bind(connection.console); -const documents: TextDocuments = new TextDocuments(TextDocument); + +const documents = new DocumentStore(connection); let rootPath: string | undefined; let tactCompiler: TactCompiler; -let enabledAsYouTypeErrorCheck = false; +let enabledAsYouTypeErrorCheck = true; let validationDelay = 1500; // flags to avoid trigger concurrent validations (compiling is slow) @@ -68,12 +71,12 @@ async function validate(document: TextDocument) { let newDocumentsWithErrors: any = []; for (let fileName in compileErrorDiagnostics) { newDocumentsWithErrors.push(URI.file(fileName).path); - connection.sendDiagnostics({diagnostics: compileErrorDiagnostics[fileName], uri: URI.file(fileName).path}); + connection.sendDiagnostics({diagnostics: compileErrorDiagnostics[fileName], uri: "file://"+URI.file(fileName).path}); } let difference = documentsWithErrors.filter((x: any) => !newDocumentsWithErrors.includes(x)); // if an error is resolved, we must to send empty diagnostics for the URI contained it; for(let item in difference) { - connection.sendDiagnostics({diagnostics: [], uri: difference[item]}); + connection.sendDiagnostics({diagnostics: [], uri: "file://"+difference[item]}); } documentsWithErrors = newDocumentsWithErrors; } @@ -86,29 +89,33 @@ async function validate(document: TextDocument) { } // This handler provides the initial list of the completion items. -connection.onCompletion((textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => { +connection.onCompletion(async (textDocumentPosition: TextDocumentPositionParams): Promise => { let completionItems: CompletionItem[] = []; - const document = documents.get(textDocumentPosition.textDocument.uri); - const service = new CompletionService(rootPath); - completionItems = completionItems.concat(service.getAllCompletionItems( document, textDocumentPosition.position )); + const document = await documents.retrieve(textDocumentPosition.textDocument.uri); + if(!document.exists) return []; + const service = new CompletionService(rootPath, documents); + completionItems = completionItems.concat(await service.getAllCompletionItems( document.document, textDocumentPosition.position )); return completionItems; }); -connection.onHover((textPosition: HoverParams): Hover => { - const hoverService = new HoverService(rootPath); - const suggestion = hoverService.getHoverItems(documents.get(textPosition.textDocument.uri), textPosition.position); - //console.log(JSON.stringify(suggestion)); +connection.onHover(async(textPosition: HoverParams): Promise => { + const hoverService = new HoverService(rootPath, documents); + const document = await documents.retrieve(textPosition.textDocument.uri); + if(!document.exists) return {contents: ""}; + const suggestion = await hoverService.getHoverItems(document.document, textPosition.position); let doc: MarkupContent = suggestion return { contents: doc } }); -connection.onDefinition((handler: TextDocumentPositionParams): Thenable | undefined => { +connection.onDefinition(async (handler: TextDocumentPositionParams): Promise => { let provider: TactDefinitionProvider; try { - provider = new TactDefinitionProvider(rootPath); - return provider.provideDefinition(documents.get(handler.textDocument.uri) as TextDocument, handler.position); + provider = new TactDefinitionProvider(rootPath, documents); + const document = await documents.retrieve(handler.textDocument.uri); + if(!document.exists) return []; + return provider.provideDefinition(document.document, handler.position); } catch(e: any) { let error: String = e.message.match(/(.*) Contract: (.*) at Line: ([0-9]*), Column: ([0-9]*)/); const compileErrorDiagnostics: Diagnostic[] = []; @@ -197,9 +204,11 @@ documents.onDidClose(event => { connection.onInitialize((result): InitializeResult => { if (result.workspaceFolders != undefined && result.workspaceFolders?.length > 0) { rootPath = Files.uriToFilePath(result.workspaceFolders[0].uri); + } else if(result.rootUri != undefined) { + rootPath = Files.uriToFilePath(result.rootUri); } - tactCompiler = new TactCompiler(rootPath ?? ""); + tactCompiler = new TactCompiler(rootPath ?? "", documents); return { capabilities: { @@ -213,7 +222,8 @@ connection.onInitialize((result): InitializeResult => { codeActionProvider: { resolveProvider: true }, - textDocumentSync: TextDocumentSyncKind.Full, + documentFormattingProvider: true, + textDocumentSync: TextDocumentSyncKind.Incremental, }, }; }); @@ -230,7 +240,7 @@ connection.onDidChangeConfiguration((change) => { startValidation(); }); -connection.onCodeAction((params: CodeActionParams): CodeAction[] | undefined => { +connection.onCodeAction(async (params: CodeActionParams): Promise => { const codeAction: CodeAction = { title: 'Dump this', kind: CodeActionKind.Refactor, @@ -238,16 +248,27 @@ connection.onCodeAction((params: CodeActionParams): CodeAction[] | undefined => }; let provider = new RefactorService(rootPath); - - codeAction.edit = provider.dump(documents.get(params.textDocument.uri) as TextDocument, params.range); + const document = await documents.retrieve(params.textDocument.uri); + if(!document.exists) return []; + codeAction.edit = provider.dump(document.document as TextDocument, params.range); return [ codeAction ]; }); -connection.onRenameRequest((params) => { +connection.onRenameRequest(async (params) => { let provider = new RefactorService(rootPath); - return provider.rename(documents.get(params.textDocument.uri) as TextDocument, params.position, params.newName); + const document = await documents.retrieve(params.textDocument.uri); + if(!document.exists) return undefined; + return provider.rename(document.document, params.position, params.newName); +}); + +connection.onDocumentFormatting(async (params) => { + const document = await documents.retrieve(params.textDocument.uri); + if (!document.exists) { + return []; + } + return formatDocument(document.document, rootPath as string); }); // Make the text document manager listen on the connection diff --git a/src/tactCompiler.ts b/src/tactCompiler.ts index bc47b6e..9c4a80a 100644 --- a/src/tactCompiler.ts +++ b/src/tactCompiler.ts @@ -4,12 +4,15 @@ import * as fs from 'fs'; import { errorToDiagnostic } from './tactErrorsToDiagnostics'; import { ContractCollection } from './model/contractsCollection'; import { check, createVirtualFileSystem, CheckResult, CheckResultItem } from '@tact-lang/compiler'; +import { DocumentStore } from './documentStore'; export class TactCompiler { public rootPath: string; + private documentStore: DocumentStore; - constructor(rootPath: string) { + constructor(rootPath: string, documentStore: DocumentStore) { this.rootPath = rootPath; + this.documentStore = documentStore; } public isRootPathSet(): boolean { @@ -38,15 +41,25 @@ export class TactCompiler { }; } - const pathKey = path.relative( this.rootPath, args.file).replaceAll('\\','/'); - const pathContent = fs.readFileSync(pathKey); + const pathKey = args.file.replaceAll('\\','/'); + + let rootPath = this.rootPath; + + // If the root path is not set, which occurs when a single file is opened separately, set the root path to the directory containing the file. + if(!this.isRootPathSet()) { + rootPath = path.dirname(args.file); + } + + const document = await this.documentStore.retrieve(pathKey); + let pathContent = (document.document)?.getText() || ""; const fsObject = {} as any; - fsObject[pathKey] = pathContent.toString('base64'); + fsObject[pathKey] = btoa(pathContent); for (let pathKey in args.sources) { - fsObject[path.relative( this.rootPath, pathKey).replaceAll('\\','/')] = Buffer.from(args.sources[pathKey].content).toString('base64'); + const fileContent = args.sources[pathKey].content || args.sources[pathKey]; + fsObject[path.relative( rootPath, pathKey).replaceAll('\\','/')] = Buffer.from(fileContent).toString('base64'); } - const result: CheckResult = check({ project: createVirtualFileSystem(path.resolve(this.rootPath).replaceAll('\\','/'), fsObject), - entrypoint: path.relative( this.rootPath, args.file).replaceAll('\\','/') + const result: CheckResult = check({ project: createVirtualFileSystem(path.resolve(rootPath).replaceAll('\\','/'), fsObject), + entrypoint: path.relative( rootPath, args.file).replaceAll('\\','/') }); return result; } @@ -73,8 +86,8 @@ export class TactCompiler { public async compileTactDocumentAndGetDiagnosticErrors(filePath: string, documentText: string) { if (this.isRootPathSet()) { - const contracts = new ContractCollection(); - contracts.addContractAndResolveImports(filePath, documentText); + const contracts = new ContractCollection(this.documentStore); + await contracts.addContractAndResolveImports(filePath, documentText); const contractsForCompilation = contracts.getDefaultContractsForCompilationDiagnostics(); const output = await this.compile(contractsForCompilation); if (output) { @@ -92,4 +105,4 @@ export class TactCompiler { return []; } -} \ No newline at end of file +} diff --git a/src/util/lruMap.ts b/src/util/lruMap.ts new file mode 100644 index 0000000..62ec82c --- /dev/null +++ b/src/util/lruMap.ts @@ -0,0 +1,39 @@ +export class LRUMap extends Map { + + constructor(private readonly _options: { size: number, dispose: (entries: [K, V][]) => void }) { + super(); + } + + set(key: K, value: V) { + super.set(key, value); + this._checkSize(); + return this; + } + + get(key: K): V | undefined { + if (!this.has(key)) { + return undefined; + } + const result = super.get(key); + this.delete(key); + this.set(key, result!); + return result; + } + + private _checkSize(): void { + setTimeout(() => { + + const slack = Math.ceil(this._options.size * .3); + + if (this.size < this._options.size + slack) { + return; + } + const result = Array.from(this.entries()).slice(0, slack); + for (let [key] of result) { + this.delete(key); + } + this._options.dispose(result); + }, 0); + } + +}