Skip to content

Commit

Permalink
✨ Exclude unsupported files on server side
Browse files Browse the repository at this point in the history
Files of unsupported languages should now never enter into the Spyglass
document lifecycle.
  • Loading branch information
SPGoding committed Feb 16, 2025
1 parent 30f732b commit 123c813
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 30 deletions.
12 changes: 3 additions & 9 deletions packages/core/src/service/MetaRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface LanguageOptions {
}

export type UriPredicate = (uri: string, ctx: UriPredicateContext) => boolean

interface LinterRegistration {
configValidator: (ruleName: string, ruleValue: unknown, logger: Logger) => unknown
linter: Linter<AstNode>
Expand Down Expand Up @@ -113,15 +114,8 @@ export class MetaRegistry {
return Array.from(this.#languages.keys())
}

public isSupportedLanguage(language: string): boolean {
return this.#languages.has(language)
}

/**
* An array of file extensions (including the leading dot (`.`)) that are supported.
*/
public getSupportedFileExtensions(): FileExtension[] {
return [...this.#languages.values()].flatMap((v) => v.extensions)
public getLanguageOptions(language: string): LanguageOptions | undefined {
return this.#languages.get(language)
}

/**
Expand Down
68 changes: 47 additions & 21 deletions packages/core/src/service/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
LinterContext,
ParserContext,
UriBinderContext,
UriPredicateContext,
} from './Context.js'
import type { Dependency } from './Dependency.js'
import { DependencyKey } from './Dependency.js'
Expand Down Expand Up @@ -81,7 +82,7 @@ export interface DocAndNode {
node: FileNode<AstNode>
}

interface DocumentEvent extends DocAndNode {}
interface DocumentEvent extends DocAndNode { }
interface DocumentErrorEvent {
errors: readonly PosRangeLanguageError[]
uri: string
Expand All @@ -90,7 +91,7 @@ interface DocumentErrorEvent {
interface FileEvent {
uri: string
}
interface EmptyEvent {}
interface EmptyEvent { }
interface RootsEvent {
roots: readonly RootUriString[]
}
Expand Down Expand Up @@ -287,10 +288,7 @@ export class Project implements ExternalEventEmitter {
* are not loaded into the memory.
*/
getTrackedFiles(): string[] {
const extensions: string[] = this.meta.getSupportedFileExtensions()
this.logger.info(`[Project#getTrackedFiles] Supported file extensions: ${extensions}`)
const supportedFiles = [...this.#dependencyFiles ?? [], ...this.#watchedFiles]
.filter((file) => extensions.includes(fileUtil.extname(file) ?? ''))
this.logger.info(
`[Project#getTrackedFiles] Listed ${supportedFiles.length} supported files`,
)
Expand Down Expand Up @@ -525,7 +523,8 @@ export class Project implements ExternalEventEmitter {

await Promise.all([listDependencyFiles(), listProjectFiles()])

this.#dependencyFiles = new Set(this.fs.listFiles())
this.#dependencyFiles = new Set([...this.fs.listFiles()]
.filter((uri) => !this.shouldExclude(uri)))
this.#dependencyRoots = new Set(this.fs.listRoots())

this.updateRoots()
Expand Down Expand Up @@ -659,13 +658,9 @@ export class Project implements ExternalEventEmitter {
this.#textDocumentCache.delete(uri)
}
private async read(uri: string): Promise<TextDocument | undefined> {
const getLanguageID = (uri: string): string => {
const ext = fileUtil.extname(uri) ?? '.plaintext'
return this.meta.getLanguageID(ext) ?? ext.slice(1)
}
const createTextDocument = async (uri: string): Promise<TextDocument | undefined> => {
const languageId = getLanguageID(uri)
if (!this.meta.isSupportedLanguage(languageId)) {
const languageId = this.guessLanguageID(uri)
if (!this.isSupportedLanguage(uri, languageId)) {
return undefined
}

Expand Down Expand Up @@ -823,7 +818,7 @@ export class Project implements ExternalEventEmitter {
linter(proxy, ctx)
}
})
;(node.linterErrors as LanguageError[]).push(...ctx.err.dump())
; (node.linterErrors as LanguageError[]).push(...ctx.err.dump())
}
} catch (e) {
this.logger.error(`[Project] [lint] Failed for ${doc.uri} # ${doc.version}`, e)
Expand Down Expand Up @@ -875,7 +870,7 @@ export class Project implements ExternalEventEmitter {
if (uri.startsWith(ArchiveUriSupporter.Protocol)) {
return // We do not accept `archive:` scheme for client-managed URIs.
}
if (this.shouldExclude(uri)) {
if (this.shouldExclude(uri, languageID)) {
return
}
const doc = TextDocument.create(uri, languageID, version, content)
Expand All @@ -899,16 +894,16 @@ export class Project implements ExternalEventEmitter {
): Promise<void> {
uri = this.normalizeUri(uri)
this.#symbolUpToDateUris.delete(uri)
if (this.shouldExclude(uri)) {
return
if (uri.startsWith(ArchiveUriSupporter.Protocol)) {
return // We do not accept `archive:` scheme for client-managed URIs.
}
const doc = this.#clientManagedDocAndNodes.get(uri)?.doc
if (!doc) {
throw new Error(
`TextDocument for ${uri} is not cached. This should not happen. Did the language client send a didChange notification without sending a didOpen one, or is there a logic error on our side resulting the 'read' function overriding the 'TextDocument' created in the 'didOpen' notification handler?`,
)
if (!doc || this.shouldExclude(uri, doc.languageId)) {
// If doc is undefined, it means the document was previously excluded by onDidOpen()
// based on the language ID supplied by the client, in which case we should return early.
// Otherwise, we perform the shouldExclude() check with the URI and the saved language ID
// as usual.
return
}
TextDocument.update(doc, changes, version)
const node = this.parse(doc)
Expand Down Expand Up @@ -966,7 +961,38 @@ export class Project implements ExternalEventEmitter {
}
}

public shouldExclude(uri: string): boolean {
/**
* Returns true iff the URI should be excluded from all Spyglass language support.
*
* @param language Optional. If ommitted, a language will be derived from the URI according to
* its file extension.
*/
public shouldExclude(uri: string, language?: string): boolean {
return !this.isSupportedLanguage(uri, language) || this.isUserExcluded(uri)
}

private isSupportedLanguage(uri: string, language?: string): boolean {
language ??= this.guessLanguageID(uri)

const languageOptions = this.meta.getLanguageOptions(language)
if (!languageOptions) {
// Unsupported language.
return false
}

const { uriPredicate } = languageOptions
return uriPredicate?.(uri, UriPredicateContext.create(this)) ?? true
}

/**
* Guess a language ID from a URI. The guessed language ID may or may not actually be supported.
*/
private guessLanguageID(uri: string): string {
const ext = fileUtil.extname(uri) ?? '.spyglassmc-unknown'
return this.meta.getLanguageID(ext) ?? ext.slice(1)
}

private isUserExcluded(uri: string): boolean {
if (this.config.env.exclude.length === 0) {
return false
}
Expand Down

0 comments on commit 123c813

Please sign in to comment.