diff --git a/src/editor-lib.ts b/src/editor-lib.ts index 4f93293..117f343 100644 --- a/src/editor-lib.ts +++ b/src/editor-lib.ts @@ -6,6 +6,11 @@ import { } from './config'; import { lastMatch } from './regular-expressions'; +export async function findProjectFiles(pattern: string = '**/*.frc'): Promise { + const files = vscode.workspace.findFiles(new vscode.RelativePattern(getProjectFolder(), pattern)); + return files; +} + /** * Search many documents to find matches for a regular expression. * @param searchRx The regular expression used to search files diff --git a/src/extension.ts b/src/extension.ts index 27e6693..1a74b62 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; import * as path from "path"; -import { DiagnosticSeverity, LanguageClient, LanguageClientOptions, TextDocumentContentChangeEvent } from "vscode-languageclient/node"; +import { DiagnosticSeverity, LanguageClient, LanguageClientOptions } from "vscode-languageclient/node"; import * as com from "./commands"; import * as ue4 from "./ue4"; import { @@ -113,21 +113,33 @@ export async function activate(context: vscode.ExtensionContext): Promise } } - vscode.workspace.onDidChangeTextDocument(changeEvent => { - const document = changeEvent.document; - if (document.languageId === 'fracas') { - diagnosticCollection.clear(); + // vscode.workspace.onDidChangeTextDocument(changeEvent => { + // const document = changeEvent.document; + // if (document.languageId === 'fracas') { + // diagnosticCollection.clear(); + // const range = document.getWordRangeAtPosition( + // vscode.window.activeTextEditor?.selection.anchor ?? new vscode.Position(0,0), + // /[#:\w\-+*.>/]+/); + // if (range) { + // const diagnostic = new vscode.Diagnostic(range, "Why'd you fuck this up?", DiagnosticSeverity.Warning); + // diagnosticCollection.set(document.uri, [diagnostic]); + // } + // } + // }); + + vscode.workspace.fs.stat + vscode.workspace.onDidSaveTextDocument(async document => { + if (document && document.languageId === "fracas") { + // diagnosticCollection.clear(); const range = document.getWordRangeAtPosition( vscode.window.activeTextEditor?.selection.anchor ?? new vscode.Position(0,0), /[#:\w\-+*.>/]+/); + if (range) { const diagnostic = new vscode.Diagnostic(range, "Why'd you fuck this up?", DiagnosticSeverity.Warning); diagnosticCollection.set(document.uri, [diagnostic]); } - } - }); - vscode.workspace.onDidSaveTextDocument(async document => { - if (document && document.languageId === "fracas") { + await com.precompileFracasFile(document); _maybeUpdateStringTables([document.uri]); } diff --git a/src/fracas/symbol-cache b/src/fracas/symbol-cache new file mode 100644 index 0000000..4dd49af --- /dev/null +++ b/src/fracas/symbol-cache @@ -0,0 +1,127 @@ +import * as vscode from "vscode"; +import { + findProjectFiles +} from '../editor-lib' +import { + FracasDefinition, + findAllIdentifiers, + findAllImportDefinitions +} from './syntax' + +class FileState { + constructor( + public readonly stat: vscode.FileStat, + public readonly importedPaths: string[], + public readonly identifiers: string[], + public readonly providedIdentifiers: string[]) + {} +} + +function _fileStateKey(fsPath: string): string { + return `fs@${fsPath}`; +} + +function _identifierKey(identifier: string): string { + return `id@${identifier}`; +} + +function _importKey(importPath: string): string { + return `imp@${importPath}`; +} + +export class FracasSymbolCache { + constructor(private storage: vscode.Memento) {} + + async updateAll(): Promise { + // collect fracas files currently in the project, and cache entries + const fileUris = await findProjectFiles(); + + // remove cache entries for files that are gone + const curPaths = new Set(fileUris.map(uri => uri.fsPath)) // the file path is the cache key + const removed = this.getFilePaths().filter(cachedPath => !curPaths.has(cachedPath)); + for (const path of removed) { + this.removeFileState(path); + } + + // update missing and stale cache entries + await Promise.all(fileUris.map(this._refreshFileStateForUri)); + } + + getFilePaths(): string[] { + return this.storage.keys().filter(key => key.startsWith('fs@')); + } + + hasFileState(path: string): boolean { + return path in this.storage.keys(); + } + + getFileState(path: string): FileState | undefined { + return this.storage.get(_fileStateKey(path)); + } + + getFileStateTimestamp(path: string): number { + const entry = this.getFileState(path); + return entry?.stat.mtime ?? 0 + } + + updateFileState(document: vscode.TextDocument, stat: vscode.FileStat): void { + this.removeFileState(document.fileName); // clear stale cache data + + const imports = findAllImportDefinitions(document); + const importedPaths = imports.map(imp => imp.location.uri.fsPath); + const identifierDefs = findAllIdentifiers(document); + const identifiers = identifierDefs.map(id => document.getText(id.range)); + + // cache reverse map of file identifiers + for (const identifier of identifiers) { + this._setFilePathForIdentifier(identifier, document.fileName); + } + + const newFileState = new FileState(stat, importedPaths, identifiers, [] /* TODO */); + this.storage.update(document.uri.fsPath, newFileState); + } + + removeFileState(path: string): void { + const entry = this.getFileState(path); + // remove all identifiers for this file + if (entry) { + for (const identifier of entry.identifiers) { + this.storage.update(_identifierKey(identifier), undefined); + } + } + + this.storage.update(path, undefined); // clear the entry. + } + + private _getImportedIdentifiersRecursive(path: string, pathsSeen: Set, identifiers: Set): void { + if (pathsSeen.has(path)) { + return; + } + pathsSeen.add(path); + const fileState = this.getFileState(path); + if (fileState) { + for (const identifier of fileState.providedIdentifiers) { + identifiers.add(identifier); + } + for (const importedPath of fileState.importedPaths) { + this._getImportedIdentifiersRecursive(importedPath, pathsSeen, identifiers); + } + } + } + + getFilePathForIdentifier(identifier: string): string | undefined { + return this.storage.get(_identifierKey(identifier)); + } + + private _setFilePathForIdentifier(identifier: string, path: string): void { + this.storage.update(_identifierKey(identifier), path); + } + + private async _refreshFileStateForUri(uri: vscode.Uri): Promise { + const stat = await vscode.workspace.fs.stat(uri); + if (!this.hasFileState(uri.fsPath) || stat.mtime > this.getFileStateTimestamp(uri.fsPath)) { + const document = await vscode.workspace.openTextDocument(uri); + this.updateFileState(document, stat); + } + } +}