Skip to content

Commit

Permalink
Add image hovers and completions documentation
Browse files Browse the repository at this point in the history
Fixes #42
  • Loading branch information
cooolbros committed Feb 5, 2025
1 parent 8f881e9 commit a6a1fe4
Show file tree
Hide file tree
Showing 29 changed files with 1,649 additions and 554 deletions.
958 changes: 928 additions & 30 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
[workspace]
members = ["packages/vtf"]
members = ["packages/vtf", "packages/vtf-canvas", "packages/vtf-png"]
resolver = "2"

[profile.release]
opt-level = 3

[workspace.dependencies]
wasm-bindgen = "0.2.95"
4 changes: 4 additions & 0 deletions apps/extension/browser/client/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default {
entry: {
extension: join(import.meta.dirname, "src/extension.ts")
},
experiments: {
asyncWebAssembly: true,
syncWebAssembly: true,
},
output: {
path: join(import.meta.dirname, "dist"),
libraryTarget: "commonjs"
Expand Down
4 changes: 4 additions & 0 deletions apps/extension/desktop/client/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export default {
entry: {
extension: join(import.meta.dirname, "src/extension.ts")
},
experiments: {
asyncWebAssembly: true,
syncWebAssembly: true,
},
output: {
path: join(import.meta.dirname, "dist"),
libraryTarget: "commonjs2"
Expand Down
2 changes: 1 addition & 1 deletion apps/vtf-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"client": "workspace:^",
"svelte": "^5.10.0",
"vite": "^6.0.3",
"vtf": "workspace:^"
"vtf-canvas": "workspace:^"
},
"type": "module"
}
4 changes: 2 additions & 2 deletions apps/vtf-editor/src/VTFViewer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { VTFEditor } from "client/VTF/VTFEditor"
import { distinctUntilChanged, filter, fromEvent, map, merge, Observable, scan, startWith, switchMap } from "rxjs"
import { toStore } from "svelte/store"
import { VTF, VTFImageFormat } from "vtf"
import { VTF, VTFImageFormat, VTFPutImageData } from "vtf-canvas"
import { z } from "zod"
interface Props {
Expand Down Expand Up @@ -83,7 +83,7 @@
function extract(node: HTMLCanvasElement, vtf: VTF) {
requestAnimationFrame(() => {
try {
vtf.putImageData(node.getContext("2d")!, vtf.header.mipmap_count - 1, 0)
VTFPutImageData(vtf, node.getContext("2d")!, vtf.header.mipmap_count - 1, 0)
} catch (error) {
console.error(error)
if (error instanceof Error) {
Expand Down
2 changes: 1 addition & 1 deletion apps/vtf-editor/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { filter, firstValueFrom, fromEvent } from "rxjs"
import { mount } from "svelte"
import init from "vtf"
import init from "vtf-canvas"
import App from "./App.svelte"
import "./app.css"

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@
"turbo": "^2.3.3",
"typescript": "^5.6.2",
"vscode-languageserver-textdocument": "^1.0.11",
"webpack": "^5.96.1",
"webpack": "^5.97.1",
"webpack-cli": "^5.1.4",
"zod": "^3.23.8"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"tsconfig": "workspace:^",
"vdf": "workspace:^",
"vdf-documentsymbols": "workspace:^",
"vscode-languageclient": "*"
"vscode-languageclient": "*",
"vtf-png": "workspace:^"
}
}
10 changes: 10 additions & 0 deletions packages/client/src/TRPCClientRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { CombinedDataTransformer, initTRPC } from "@trpc/server"
import { Uri } from "common/Uri"
import { firstValueFrom } from "rxjs"
import { commands, languages, window, workspace } from "vscode"
import { VTF, VTFToPNG } from "vtf-png"
import { z } from "zod"
import { decorationTypes, editorDecorations } from "./decorations"
import { searchForHUDRoot } from "./searchForHUDRoot"
Expand Down Expand Up @@ -131,6 +132,15 @@ export function TRPCClientRouter(
fileSystems.delete(input.key)
})
}),
VTFToPNG: t
.procedure.input(
z.object({
uri: Uri.schema
})
).mutation(async ({ input }) => {
const vtf = new VTF(await workspace.fs.readFile(input.uri))
return VTFToPNG(vtf, 256)
}),
window: t.router({
createTextEditorDecorationType: t
.procedure
Expand Down
52 changes: 49 additions & 3 deletions packages/server/src/LanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { devalueTransformer } from "common/devalueTransformer"
import { Uri } from "common/Uri"
import { VSCodeVDFConfigurationSchema, type VSCodeVDFConfiguration } from "common/VSCodeVDFConfiguration"
import { VSCodeVDFLanguageNameSchema, type VSCodeVDFLanguageID } from "common/VSCodeVDFLanguageID"
import { posix } from "path"
import { BehaviorSubject, concatMap, defer, distinctUntilChanged, distinctUntilKeyChanged, firstValueFrom, from, map, Observable, shareReplay, Subject, Subscription, switchMap, tap, zip } from "rxjs"
import { findBestMatch } from "string-similarity"
import { VDFPosition, VDFRange } from "vdf"
import type { FileType } from "vscode"
import { CodeAction, CodeActionKind, CodeLensRefreshRequest, CompletionItem, CompletionItemKind, Diagnostic, DidChangeConfigurationNotification, DocumentLink, DocumentSymbol, TextDocumentSyncKind, TextEdit, WorkspaceEdit, type CodeActionParams, type CodeLensParams, type CompletionParams, type Connection, type DefinitionParams, type DidSaveTextDocumentParams, type DocumentFormattingParams, type DocumentLinkParams, type DocumentSymbolParams, type GenericRequestHandler, type PrepareRenameParams, type ReferenceParams, type RenameParams, type ServerCapabilities, type TextDocumentChangeEvent } from "vscode-languageserver"
import { CodeAction, CodeActionKind, CodeLensRefreshRequest, CompletionItem, CompletionItemKind, Diagnostic, DidChangeConfigurationNotification, DocumentLink, DocumentSymbol, MarkupKind, TextDocumentSyncKind, TextEdit, WorkspaceEdit, type CodeActionParams, type CodeLensParams, type CompletionParams, type Connection, type DefinitionParams, type DidSaveTextDocumentParams, type DocumentFormattingParams, type DocumentLinkParams, type DocumentSymbolParams, type GenericRequestHandler, type PrepareRenameParams, type ReferenceParams, type RenameParams, type ServerCapabilities, type TextDocumentChangeEvent } from "vscode-languageserver"
import { z } from "zod"
import { Definitions, References } from "./DefinitionReferences"
import type { HUDAnimationsLanguageServer } from "./HUDAnimations/HUDAnimationsLanguageServer"
Expand All @@ -30,7 +31,8 @@ const capabilities = {
"/",
"\"",
"#",
]
],
resolveProvider: true,
},
definitionProvider: true,
referencesProvider: true,
Expand Down Expand Up @@ -67,6 +69,7 @@ export interface CompletionFilesOptions {
value: string | null
extensionsPattern: `.${string}` | null
callbackfn?: (name: string, type: FileType) => Partial<Omit<CompletionItem, "label" | "kind" | "sortText">> | null
image?: boolean
}

export abstract class LanguageServer<
Expand Down Expand Up @@ -277,6 +280,7 @@ export abstract class LanguageServer<

this.onTextDocumentRequest(this.connection.onDidSaveTextDocument, this.onDidSaveTextDocument)
this.onTextDocumentRequest(this.connection.onCompletion, this.onCompletion)
this.connection.onCompletionResolve((item) => this.onCompletionResolve(item))
this.onTextDocumentRequest(this.connection.onDefinition, this.onDefinition)
this.onTextDocumentRequest(this.connection.onReferences, this.onReferences)
this.onTextDocumentRequest(this.connection.onDocumentSymbol, this.onDocumentSymbol)
Expand Down Expand Up @@ -471,6 +475,23 @@ export abstract class LanguageServer<
})
}

protected async VTFToPNG(uri: Uri, path: string) {
try {
using document = await this.documents.get(uri, true)
return await firstValueFrom(
document.fileSystem$.pipe(
switchMap((fileSystem) => fileSystem.resolveFile(path)),
concatMap(async (uri) => uri != null ? await this.trpc.servers.vmt.baseTexture.query({ uri }) : null),
concatMap(async (uri) => uri != null ? this.trpc.client.VTFToPNG.mutate({ uri }) : null),
)
)
}
catch (error) {
console.error(error)
return null
}
}

protected async onDidOpen(event: TextDocumentChangeEvent<TDocument>): Promise<{ onDidClose: () => void }> {
return {
onDidClose: () => {
Expand Down Expand Up @@ -555,7 +576,7 @@ export abstract class LanguageServer<
const items = await this.getCompletion(
document,
new VDFPosition(params.position.line, params.position.character),
async (path: string, { value, extensionsPattern, callbackfn }: CompletionFilesOptions) => {
async (path: string, { value, extensionsPattern, callbackfn, image }: CompletionFilesOptions) => {
return await firstValueFrom(
zip([document.fileSystem$, document.documentConfiguration$]).pipe(
concatMap(async ([fileSystem, documentConfiguration]) => {
Expand Down Expand Up @@ -606,6 +627,14 @@ export abstract class LanguageServer<
...(incremental && {
commitCharacters: ["/"],
}),
...(image && type == 1 && {
data: {
image: {
uri: document.uri,
path: posix.join(path, name)
}
},
}),
...rest
}
}
Expand Down Expand Up @@ -636,6 +665,23 @@ export abstract class LanguageServer<

protected abstract getCompletion(document: TDocument, position: VDFPosition, files: CompletionFiles): Promise<CompletionItem[] | null>

protected async onCompletionResolve(item: CompletionItem): Promise<CompletionItem> {
const result = z.object({ image: z.object({ uri: Uri.schema, path: z.string() }) }).safeParse(item.data)

if (result.success) {
const { image } = result.data
const uri = await this.VTFToPNG(image.uri, image.path)
if (uri) {
item.documentation = {
kind: MarkupKind.Markdown,
value: `![](${uri})`
}
}
}

return item
}

private async onDefinition(params: TextDocumentRequestParams<DefinitionParams>) {
using document = await this.documents.get(params.textDocument.uri)
const definitionReferences = await firstValueFrom(document.definitionReferences$)
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/VDF/Popfile/PopfileTextDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { DiagnosticCodeAction } from "../../LanguageServer"
import type { TeamFortress2FileSystem } from "../../TeamFortress2FileSystem"
import type { TextDocumentInit } from "../../TextDocumentBase"
import type { TextDocuments } from "../../TextDocuments"
import { VDFTextDocument, type VDFTextDocumentDependencies, type VDFTextDocumentSchema } from "../VDFTextDocument"
import { VDFTextDocument, VGUIAssetType, type VDFTextDocumentDependencies, type VDFTextDocumentSchema } from "../VDFTextDocument"
import colours from "./colours.json"
import keys from "./keys.json"
import values from "./values.json"
Expand Down Expand Up @@ -82,6 +82,7 @@ export class PopfileTextDocument extends VDFTextDocument<PopfileTextDocument> {
return null
}
},
asset: VGUIAssetType.Image
},
{
name: "sound",
Expand Down
42 changes: 37 additions & 5 deletions packages/server/src/VDF/VDFLanguageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { firstValueFrom, type Observable } from "rxjs"
import { VDFIndentation, VDFNewLine, VDFPosition } from "vdf"
import { VDFDocumentSymbols } from "vdf-documentsymbols"
import { formatVDF, type VDFFormatStringifyOptions } from "vdf-format"
import { Color, CompletionItem, CompletionItemKind, Range, TextEdit, type ColorPresentationParams, type Connection, type DocumentColorParams, type DocumentFormattingParams, type ServerCapabilities, type TextDocumentChangeEvent } from "vscode-languageserver"
import type { z } from "zod"
import { Color, CompletionItem, CompletionItemKind, Hover, Range, TextEdit, type ColorPresentationParams, type Connection, type DocumentColorParams, type DocumentFormattingParams, type HoverParams, type ServerCapabilities, type TextDocumentChangeEvent } from "vscode-languageserver"
import { z } from "zod"
import { LanguageServer, type CompletionFiles, type TextDocumentRequestParams } from "../LanguageServer"
import type { TextDocumentInit } from "../TextDocumentBase"
import type { VDFTextDocument, VDFTextDocumentDependencies } from "./VDFTextDocument"
import { resolveFileDetail, VGUIAssetType, type VDFTextDocument, type VDFTextDocumentDependencies } from "./VDFTextDocument"

export interface VDFLanguageServerConfiguration<TDocument extends VDFTextDocument<TDocument>> {
name: "popfile" | "vdf" | "vmt"
Expand All @@ -31,9 +31,10 @@ export abstract class VDFLanguageServer<

constructor(languageId: TLanguageId, name: z.infer<typeof VSCodeVDFLanguageNameSchema>[TLanguageId], connection: Connection, VDFLanguageServerConfiguration: VDFLanguageServerConfiguration<TDocument>) {
super(languageId, name, connection, {
servers: VDFLanguageServerConfiguration.servers,
servers: new Set(["vmt", ...VDFLanguageServerConfiguration.servers]),
capabilities: {
...VDFLanguageServerConfiguration.capabilities,
hoverProvider: true,
colorProvider: true,
},
createDocument: async (init, documentConfiguration$, refCountDispose) => await VDFLanguageServerConfiguration.createDocument(init, documentConfiguration$, refCountDispose)
Expand All @@ -42,6 +43,7 @@ export abstract class VDFLanguageServer<
this.VDFLanguageServerConfiguration = VDFLanguageServerConfiguration
this.documentsColours = new Map()

this.onTextDocumentRequest(this.connection.onHover, this.onHover)
this.onTextDocumentRequest(this.connection.onDocumentColor, this.onDocumentColor)
this.onTextDocumentRequest(this.connection.onColorPresentation, this.onColorPresentation)
}
Expand Down Expand Up @@ -185,7 +187,8 @@ export abstract class VDFLanguageServer<
const { dir, name: nameNoExt } = posix.parse(name)
return posix.join(dir, nameNoExt)
})
: undefined
: undefined,
image: fileConfiguration.asset == VGUIAssetType.Image
})
}

Expand Down Expand Up @@ -270,6 +273,35 @@ export abstract class VDFLanguageServer<
}
}

private async onHover(params: TextDocumentRequestParams<HoverParams>): Promise<Hover | null> {
using document = await this.documents.get(params.textDocument.uri)
const documentSymbols = await firstValueFrom(document.documentSymbols$)

const documentSymbol = documentSymbols.getDocumentSymbolAtPosition(params.position)
if (!documentSymbol || !documentSymbol.detailRange || !documentSymbol.detailRange.contains(params.position)) {
return null
}

const schema = (await firstValueFrom(document.configuration.dependencies$)).schema
const fileSchema = schema.files.find(({ keys }) => keys.has(documentSymbol.key.toLowerCase()))
if (!fileSchema) {
return null
}

if (fileSchema.asset == VGUIAssetType.Image) {
const path = resolveFileDetail(documentSymbol.detail!, fileSchema)
const uri = await this.VTFToPNG(document.uri, path)
if (uri) {
return {
contents: `![](${uri})`,
range: documentSymbol.detailRange
}
}
}

return null
}

private async onDocumentColor(params: TextDocumentRequestParams<DocumentColorParams>) {

using document = await this.documents.get(params.textDocument.uri)
Expand Down
9 changes: 7 additions & 2 deletions packages/server/src/VDF/VDFTextDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function ArrayEndsWithArray<T1, T2>(arr1: T1[], arr2: T2[], comparer: (a: T1, b:
return arr2.every((value, index) => comparer(arr1[start + index], value))
}

function resolveFileDetail(detail: string, configuration: VDFTextDocumentSchema["files"][number]) {
export function resolveFileDetail(detail: string, configuration: VDFTextDocumentSchema["files"][number]) {
const [basename, ...rest] = detail.replaceAll(/[/\\]+/g, "/").split("/").reverse()

return posix.resolve(
Expand Down Expand Up @@ -87,7 +87,7 @@ export interface VDFTextDocumentSchema {
extensionsPattern: `.${string}` | null
resolveBaseName: (value: string, withExtension: (extension: `.${string}`) => string) => string,
toCompletionItem?: (name: string, type: number, withoutExtension: () => string) => Partial<Omit<CompletionItem, "kind">> | null,

asset?: VGUIAssetType
}[]
colours: {
keys: {
Expand Down Expand Up @@ -120,6 +120,11 @@ export interface DefinitionResult {
nameRange?: VDFRange
}

export const enum VGUIAssetType {
None = 0,
Image = 1
}

export abstract class VDFTextDocument<TDocument extends VDFTextDocument<TDocument>> extends TextDocumentBase<VDFDocumentSymbols, VDFTextDocumentDependencies> {

public readonly configuration: VDFTextDocumentConfiguration<TDocument>
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/VDF/VGUI/schemas/ClientSchemeSchema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { VDFDocumentSymbol } from "vdf-documentsymbols"
import { CompletionItemKind } from "vscode-languageserver"
import type { DefinitionMatcher, DefinitionResult, VDFTextDocumentSchema } from "../../VDFTextDocument"
import { VGUIAssetType, type DefinitionMatcher, type DefinitionResult, type VDFTextDocumentSchema } from "../../VDFTextDocument"

class SchemeDefinitionMatcher implements DefinitionMatcher {

Expand Down Expand Up @@ -132,6 +132,7 @@ export const ClientSchemeSchema: VDFTextDocumentSchema = {
folder: "materials/vgui",
extensionsPattern: ".vmt",
resolveBaseName: (value, withExtension) => withExtension(".vmt"),
asset: VGUIAssetType.Image
},
],
colours: {
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/VDF/VGUI/schemas/VGUISchema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CompletionItemKind } from "vscode-languageserver"
import type { VDFTextDocumentSchema } from "../../VDFTextDocument"
import { VGUIAssetType, type VDFTextDocumentSchema } from "../../VDFTextDocument"
import clientscheme from "../clientscheme.json"
import keys from "../keys.json"
import values from "../values.json"
Expand Down Expand Up @@ -105,6 +105,7 @@ export const VGUISchema: VDFTextDocumentSchema = {
extensionsPattern: ".vmt",
resolveBaseName: (value, withExtension) => withExtension(".vmt"),
toCompletionItem: (name, type, withoutExtension) => ({ insertText: withoutExtension() }),
asset: VGUIAssetType.Image
},
{
name: "sound",
Expand Down
Loading

0 comments on commit a6a1fe4

Please sign in to comment.