Add clangd language server support (#739)

* Initial clangd support
Tidy up languageserver.ts
Add CompilerCommand option

* Codemirror tooltip newline handling

* Force github login through redirects instead of popup window

* Gracefully exit LS when scratch unloads

* Break scratch language out to api, move language server integration to new hook

* Fix broken import

* Fix languageServer.ts import warnings
(and rename languageserver.ts to languageServer.ts)

* Format on Alt-Shift-F

* Add default, site-wide .clang-format

* Allow source to reference context definitions

* Remove overzealous IIFEs

* Clean up yaml imports, useLanguageServer state

* Remove webpack yaml-specific config

* Fix default-clang-format.yaml loading

* Add editor option to enable / disable language server

* Change language server setting text

* Add ability to select small language server, version bump

* EditorSettings clean

* EditorSettings 2: Electric Boogaloo (sponsored by tailwind)

* Fix tooltip appearance

* Remove alignment settings from default-clang-format.yaml

* Remove references to preferSmallLanguageServer

---------

Co-authored-by: ConorBobbleHat <c.github@firstpartners.net>
Co-authored-by: Mark Street <streetster@gmail.com>
This commit is contained in:
ConorB
2023-06-04 06:15:27 +01:00
committed by GitHub
parent b523916486
commit 3b39a6d3af
24 changed files with 1161 additions and 58 deletions

View File

@@ -1,4 +1,3 @@
import enum
import logging
from dataclasses import dataclass, field
from functools import cache
@@ -18,6 +17,7 @@ from coreapp.flags import (
COMMON_MWCC_FLAGS,
COMMON_GCC_SATURN_FLAGS,
Flags,
Language,
)
from coreapp.platforms import (
@@ -44,23 +44,6 @@ CONFIG_PY = "config.py"
COMPILER_BASE_PATH: Path = settings.COMPILER_BASE_PATH
class Language(enum.Enum):
C = "C"
OLD_CXX = "C++"
CXX = "C++"
PASCAL = "Pascal"
ASSEMBLY = "Assembly"
def get_file_extension(self) -> str:
return {
Language.C: "c",
Language.CXX: "cpp",
Language.OLD_CXX: "c++",
Language.PASCAL: "p",
Language.ASSEMBLY: "s",
}[self]
@dataclass(frozen=True)
class Compiler:
id: str

View File

@@ -1,9 +1,29 @@
import enum
from dataclasses import dataclass
from typing import Dict, List, Union
ASMDIFF_FLAG_PREFIX = "-DIFF"
class Language(enum.Enum):
C = "C"
OLD_CXX = "C++"
CXX = "C++"
PASCAL = "Pascal"
ASSEMBLY = "Assembly"
OBJECTIVE_C = "ObjectiveC"
def get_file_extension(self) -> str:
return {
Language.C: "c",
Language.CXX: "cpp",
Language.OLD_CXX: "c++",
Language.PASCAL: "p",
Language.ASSEMBLY: "s",
Language.OBJECTIVE_C: "m",
}[self]
@dataclass(frozen=True)
class Checkbox:
id: str
@@ -30,13 +50,31 @@ class FlagSet:
}
Flags = List[Union[Checkbox, FlagSet]]
@dataclass(frozen=True)
class LanguageFlagSet:
id: str
flags: Dict[str, Language]
def to_json(self) -> Dict[str, Union[str, List[str]]]:
# To the client, we're a regular FlagSet - the extra metadata we carry
# is purely for the backend to determine the scratch's language
return {
"type": "flagset",
"id": self.id,
"flags": list(self.flags.keys()),
}
Flags = List[Union[Checkbox, FlagSet, LanguageFlagSet]]
COMMON_ARMCC_FLAGS: Flags = [
FlagSet(
id="armcc_opt_level", flags=["-O0", "-O1", "-O2", "-O3", "-Ospace", "-Otime"]
),
FlagSet(id="armcc_language", flags=["--c90", "--c99", "--cpp"]),
LanguageFlagSet(
id="armcc_language",
flags={"--c90": Language.C, "--c99": Language.C, "--cpp": Language.CXX},
),
FlagSet(id="armcc_instset", flags=["--arm", "--thumb"]),
Checkbox(id="armcc_debug", flag="--debug"),
]
@@ -46,7 +84,9 @@ COMMON_CLANG_FLAGS: Flags = [
id="clang_opt_level", flags=["-O0", "-O1", "-O2", "-O3", "-Ofast", "-Os", "-Oz"]
),
FlagSet(id="clang_debug_level", flags=["-g0", "-g1", "-g2", "-g3"]),
FlagSet(id="clang_language", flags=["-x c++", "-x c"]),
LanguageFlagSet(
id="clang_language", flags={"-x c++": Language.CXX, "-x c": Language.C}
),
FlagSet(
id="clang_language_standard",
flags=[
@@ -136,9 +176,15 @@ COMMON_MWCC_FLAGS: Flags = [
id="mwcc_string_constant_options",
flags=["-str reuse", "-str pool", "-str readonly", "-str reuse,pool,readonly"],
),
FlagSet(
LanguageFlagSet(
id="mwcc_language",
flags=["-lang=c", "-lang=c++", "-lang=c99", "-lang=ec++", "-lang=objc"],
flags={
"-lang=c": Language.C,
"-lang=c++": Language.CXX,
"-lang=c99": Language.C,
"-lang=ec++": Language.CXX,
"-lang=objc": Language.OBJECTIVE_C,
},
),
FlagSet(id="mwcc_char_signedness", flags=["-char signed", "-char unsigned"]),
Checkbox(id="mwcc_cpp_exceptions_off", flag="-Cpp_exceptions off"),

View File

@@ -16,6 +16,9 @@ from .models.profile import Profile
from .models.project import Project, ProjectFunction, ProjectImportConfig, ProjectMember
from .models.scratch import CompilerConfig, Scratch
from .flags import LanguageFlagSet
from . import compilers
def serialize_profile(
request: Request, profile: Profile, small: bool = False
@@ -142,6 +145,7 @@ class ScratchSerializer(serializers.HyperlinkedModelSerializer):
context = serializers.CharField(allow_blank=True, trim_whitespace=False) # type: ignore
project = serializers.SerializerMethodField()
project_function = serializers.SerializerMethodField()
language = serializers.SerializerMethodField()
class Meta:
model = Scratch
@@ -183,6 +187,38 @@ class ScratchSerializer(serializers.HyperlinkedModelSerializer):
)
return None
def get_language(self, scratch: Scratch) -> Optional[str]:
"""
Strategy for extracting a scratch's language:
- If the scratch's compiler has a LanguageFlagSet in its flags, attempt to match a language flag against that
- Otherwise, fallback to the compiler's default language
"""
compiler = compilers.from_id(scratch.compiler)
language_flag_set = next(
iter([i for i in compiler.flags if isinstance(i, LanguageFlagSet)]),
None,
)
if language_flag_set:
language = next(
iter(
[
language
for (flag, language) in language_flag_set.flags.items()
if flag in scratch.compiler_flags
]
),
None,
)
if language:
return language.value
# If we're here, either the compiler doesn't have a LanguageFlagSet, or the scratch doesn't
# have a flag within it.
# Either way: fall back to the compiler default.
return compiler.language.value
class TerseScratchSerializer(ScratchSerializer):
owner = TerseProfileField(read_only=True)
@@ -198,6 +234,7 @@ class TerseScratchSerializer(ScratchSerializer):
"creation_time",
"platform",
"compiler",
"language",
"name",
"score",
"max_score",

View File

@@ -12,7 +12,8 @@ from django.test.testcases import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from coreapp.compilers import DummyCompiler, Language
from coreapp.compilers import DummyCompiler
from coreapp.flags import Language
from coreapp.sandbox import Sandbox
from coreapp import compilers, platforms

View File

@@ -20,7 +20,7 @@ from rest_framework.viewsets import GenericViewSet
from coreapp import compilers, platforms
from ..compiler_wrapper import CompilationResult, CompilerWrapper, DiffResult
from ..decompiler_wrapper import DecompilerWrapper
from ..compilers import Language
from ..flags import Language
from ..decorators.django import condition

View File

@@ -72,7 +72,14 @@ let app = withPlausibleProxy({
key: "X-DNS-Prefetch-Control",
value: "on",
},
{
key: "Cross-Origin-Opener-Policy",
value:"same-origin",
},
{
key: "Cross-Origin-Embedder-Policy",
value: "require-corp",
},
],
},
]
@@ -83,6 +90,27 @@ let app = withPlausibleProxy({
use: ["@svgr/webpack"],
})
// @open-rpc/client-js brings in some dependencies which, in turn, have optional dependencies.
// This confuses the heck out of webpack, so tell it should just sub in a CommonJS-style "require" statement
// instead (which will fail and trigger the fallback at runtime)
// https://stackoverflow.com/questions/58697934/webpack-how-do-you-require-an-optional-dependency-in-bundle-saslprep
config.externals.push({
"encoding": "commonjs encoding",
"bufferutil": "commonjs bufferutil",
"utf-8-validate": "commonjs utf-8-validate",
})
// All of the vscode-* packages (jsonrpc, languageserver-protocol, etc.) are distributed as UMD modules.
// This also leaves webpack with no idea how to handle require statements.
// umd-compat-loader strips away the hedaer UMD adds to allow browsers to parse ES modules
// and just treats the importee as an ES module.
config.module.rules.push({
"test": /node_modules[\\|/](vscode-.*)/,
"use": {
"loader": "umd-compat-loader",
},
})
return config
},
images: {

View File

@@ -10,6 +10,7 @@
"clean": "rm -rf .next node_modules"
},
"dependencies": {
"@clangd-wasm/clangd-wasm": "15.0.7-dev5",
"@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-cpp": "^6.0.2",
@@ -20,6 +21,7 @@
"@codemirror/view": "^6.10.1",
"@lezer/common": "^1.0.2",
"@lezer/highlight": "^1.1.4",
"@open-rpc/client-js": "^1.8.1",
"@primer/octicons-react": "^18.3.0",
"@radix-ui/colors": "^0.1.8",
"@react-hook/resize-observer": "^1.2.6",
@@ -49,7 +51,8 @@
"sharp": "^0.32.1",
"swr": "^1.3.0",
"use-debounce": "^9.0.4",
"use-persisted-state": "^0.3.3"
"use-persisted-state": "^0.3.3",
"vscode-languageserver-protocol": "^3.17.3"
},
"devDependencies": {
"@babel/core": "^7.21.8",
@@ -86,6 +89,7 @@
"stylelint-config-standard-scss": "^9.0.0",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.4",
"umd-compat-loader": "^2.1.2",
"webpack": "^5.81.0"
},
"browserslist": [

View File

@@ -48,7 +48,7 @@ export default function WelcomeInfo() {
<ArrowRightIcon />
</Button>
</Link>
<GitHubLoginButton popup />
<GitHubLoginButton />
</div>
<div className="my-6 hidden sm:block">
<SiteStats />

View File

@@ -64,7 +64,7 @@ export default function Page() {
<p className="py-4">
You can try again by clicking the button below.
</p>
<GitHubLoginButton popup={false} />
<GitHubLoginButton />
</div> : code ? <div className="flex items-center justify-center gap-4 py-8 text-2xl font-medium text-gray-12">
<LoadingSpinner width={32} className="animate-spin" />
Signing in...
@@ -72,7 +72,7 @@ export default function Page() {
<p>
Sign in to decomp.me
</p>
<GitHubLoginButton popup={false} />
<GitHubLoginButton />
</div>}
</main>
}

View File

@@ -20,7 +20,7 @@ export default function UserState() {
{isAnonUser(user) ? "You appear as" : "Signed in as"} <UserMention user={user} />
</p>
<div className="flex items-center gap-2 pt-2">
{isAnonUser(user) && <GitHubLoginButton popup />}
{isAnonUser(user) && <GitHubLoginButton />}
<SignOutButton />
</div>
</div> : <div className="flex animate-pulse items-center gap-2">

View File

@@ -1,5 +1,7 @@
"use client"
import { useEffect, useRef, useState } from "react"
import LoadingSpinner from "@/components/loading.svg"
import * as settings from "@/lib/settings"
import Checkbox from "../Checkbox"
@@ -9,6 +11,31 @@ import SliderField from "../SliderField"
export default function EditorSettings() {
const [autoRecompile, setAutoRecompile] = settings.useAutoRecompileSetting()
const [autoRecompileDelay, setAutoRecompileDelay] = settings.useAutoRecompileDelaySetting()
const [languageServerEnabled, setLanguageServerEnabled] = settings.useLanguageServerEnabled()
const [downloadingLanguageServer, setDownloadingLanguageServer] = useState(false)
const isInitialMount = useRef(true)
useEffect(() => {
// Prevent the language server binary from being downloaded if the user has it enabled, then enters settings
if (isInitialMount.current) {
isInitialMount.current = false
return
}
if (languageServerEnabled) {
setDownloadingLanguageServer(true)
import("@clangd-wasm/clangd-wasm").then(({ ClangdStdioTransport }) => {
// We don't need to do anything with the result of this fetch - all this
// is is a way to make sure the wasm file ends up in the browser's cache.
fetch(ClangdStdioTransport.getDefaultWasmURL(false))
.then(res => res.blob())
.then(() => setDownloadingLanguageServer(false))
})
}
}, [languageServerEnabled])
return <>
<Section title="Automatic compilation">
@@ -32,5 +59,15 @@ export default function EditorSettings() {
</div>
</Checkbox>
</Section>
<Section title="Language server">
<Checkbox
checked={languageServerEnabled}
onChange={setLanguageServerEnabled}
label="Enable language server"
description="Enable editor features such as code completion, error checking, and formatting via clangd and WebAssembly magic. WARNING: enabling will incur a one time ~13MB download, and bump up resource usage during editing.">
{downloadingLanguageServer && <div className="flex gap-2 p-4"><LoadingSpinner width="24px" /> Downloading...</div>}
</Checkbox>
</Section>
</>
}

View File

@@ -14,12 +14,17 @@
}
:global(.cm-gutters),
:global(.cm-content) {
:global(.cm-content),
:global(.cm-tooltip-hover) {
font-size: var(--cm-font-size, 0.8rem);
font-family: var(--monospace);
line-height: var(--code-line-height, 1.5);
}
:global(.cm-tooltip-section) {
padding: 1em;
}
:global(.cm-gutters) {
color: var(--code-foreground) !important;
}

View File

@@ -9,7 +9,7 @@ import Button from "./Button"
const DEFAULT_SCOPE_STR = ""
export default function GitHubLoginButton({ label, popup, className }: { label?: string, popup: boolean, className?: string }) {
export default function GitHubLoginButton({ label, className }: { label?: string, className?: string }) {
const user = useThisUser()
if (user && !isAnonUser(user)) {
@@ -18,7 +18,7 @@ export default function GitHubLoginButton({ label, popup, className }: { label?:
}
if (isGitHubLoginSupported()) {
return <Button className={className} onClick={() => showGitHubLoginWindow(popup, DEFAULT_SCOPE_STR)}>
return <Button className={className} onClick={() => showGitHubLoginWindow(DEFAULT_SCOPE_STR)}>
<MarkGithubIcon size={16} /> {label ?? "Sign in with GitHub"}
</Button>
} else {

View File

@@ -32,7 +32,7 @@ export default function LoginState({ className }: { className?: string }) {
}
if (api.isAnonUser(user)) {
return <GitHubLoginButton popup label="Sign in" />
return <GitHubLoginButton label="Sign in" />
}
return <button

View File

@@ -18,7 +18,7 @@ export default function UserMenuItems() {
</div>
</MenuItem>
<MenuItem>
<GitHubLoginButton popup />
<GitHubLoginButton />
</MenuItem>
</>
}

View File

@@ -7,7 +7,7 @@ import * as api from "@/lib/api"
import basicSetup from "@/lib/codemirror/basic-setup"
import useCompareExtension from "@/lib/codemirror/useCompareExtension"
import { useSize } from "@/lib/hooks"
import { useAutoRecompileSetting, useAutoRecompileDelaySetting } from "@/lib/settings"
import { useAutoRecompileSetting, useAutoRecompileDelaySetting, useLanguageServerEnabled } from "@/lib/settings"
import CompilerOpts from "../compiler/CompilerOpts"
import CustomLayout, { activateTabInLayout, Layout } from "../CustomLayout"
@@ -20,6 +20,7 @@ import { Tab, TabCloseButton } from "../Tabs"
import AboutScratch from "./AboutScratch"
import DecompilationPanel from "./DecompilePanel"
import FamilyPanel from "./FamilyPanel"
import useLanguageServer from "./hooks/useLanguageServer"
import styles from "./Scratch.module.scss"
import ScratchMatchBanner from "./ScratchMatchBanner"
import ScratchToolbar from "./ScratchToolbar"
@@ -129,6 +130,7 @@ export default function Scratch({
const [autoRecompileSetting] = useAutoRecompileSetting()
const [autoRecompileDelaySetting] = useAutoRecompileDelaySetting()
const [languageServerEnabledSetting] = useLanguageServerEnabled()
const { compilation, isCompiling, isCompilationOld, compile } = api.useCompilation(scratch, autoRecompileSetting, autoRecompileDelaySetting, initialCompilation)
const userIsYou = api.useUserIsYou()
const [selectedSourceLine, setSelectedSourceLine] = useState<number | null>()
@@ -146,6 +148,8 @@ export default function Scratch({
const sourceCompareExtension = useCompareExtension(sourceEditor, shouldCompare ? parentScratch?.source_code : undefined)
const contextCompareExtension = useCompareExtension(contextEditor, shouldCompare ? parentScratch?.context : undefined)
const [saveSource, saveContext] = useLanguageServer(languageServerEnabledSetting, scratch, sourceEditor, contextEditor)
// TODO: CustomLayout should handle adding/removing tabs
const [decompilationTabEnabled, setDecompilationTabEnabled] = useState(false)
useEffect(() => {
@@ -177,7 +181,10 @@ export default function Scratch({
key={id}
tabKey={id}
label="Source code"
onSelect={() => sourceEditor.current?.focus?.()}
onSelect={() => {
sourceEditor.current?.focus?.()
saveContext()
}}
>
<CodeMirror
viewRef={sourceEditor}
@@ -197,7 +204,10 @@ export default function Scratch({
tabKey={id}
label="Context"
className={styles.context}
onSelect={() => contextEditor.current?.focus?.()}
onSelect={() => {
contextEditor.current?.focus?.()
saveSource()
}}
>
<CodeMirror
viewRef={contextEditor}

View File

@@ -0,0 +1,36 @@
# Based on https://github.com/pmret/papermario/blob/main/.clang-format
DisableFormat: true # clang-format supports many filetypes
---
Language: Cpp
DisableFormat: false
BasedOnStyle: WebKit
IndentWidth: 4
UseTab: Never
ColumnLimit: 120
AllowShortIfStatementsOnASingleLine: false
AllowShortBlocksOnASingleLine: false
BreakBeforeBraces: Custom
BraceWrapping:
# Place opening brace on next line if the statement is multi-line
# e.g.
# if (very
# long
# condition)
# {
AfterControlStatement: MultiLine
IndentCaseLabels: true
BreakBeforeBinaryOperators: NonAssignment
ExperimentalAutoDetectBinPacking: true
BinPackParameters: false
AlignAfterOpenBracket: BlockIndent
AllowShortFunctionsOnASingleLine: None
---
...

View File

@@ -0,0 +1,131 @@
import { MutableRefObject, useEffect, useState } from "react"
import type { ClangdStdioTransport, CompileCommands } from "@clangd-wasm/clangd-wasm"
import { StateEffect } from "@codemirror/state"
import { EditorView } from "codemirror"
import * as api from "@/lib/api"
import { LanguageServerClient, languageServerWithTransport } from "@/lib/codemirror/languageServer"
export default function useLanguageServer(enabled: boolean, scratch: api.Scratch, sourceEditor: MutableRefObject<EditorView>, contextEditor: MutableRefObject<EditorView>) {
const [initialScratchState, setInitialScratchState] = useState<api.Scratch>(undefined)
const [defaultClangFormat, setDefaultClangFormat] = useState<string>(undefined)
const [ClangdStdioTransportModule, setClangdStdioTransportModule] = useState<typeof ClangdStdioTransport>(undefined)
const [saveSource, setSaveSource] = useState<(string) => Promise<void>>(undefined)
const [saveContext, setSaveContext] = useState<(string) => Promise<void>>(undefined)
useEffect(() => {
const loadClangdModule = async () => {
if (!enabled) return
if (!(scratch.language == "C" || scratch.language == "C++")) return
const { ClangdStdioTransport } = await import("@clangd-wasm/clangd-wasm")
setClangdStdioTransportModule(() => ClangdStdioTransport)
}
loadClangdModule()
}, [scratch.language, enabled])
useEffect(() => {
if (!initialScratchState) {
setInitialScratchState(scratch)
}
}, [scratch, initialScratchState])
useEffect(() => {
fetch(new URL("./default-clang-format.yaml", import.meta.url))
.then(res => res.text())
.then(setDefaultClangFormat)
}, [])
// We break this out into a seperate effect from the module loading
// because if we had _lsClient defined inside an async function, we wouldn't be
// able to reference it inside of the destructor.
useEffect(() => {
if (!ClangdStdioTransportModule) return
if (!initialScratchState) return
if (!defaultClangFormat) return
const languageId = {
"C": "c",
"C++": "cpp",
}[initialScratchState.language]
const sourceFilename = `source.${languageId}`
const contextFilename = `context.${languageId}`
const compileCommands: CompileCommands = [
{
directory: "/",
file: sourceFilename,
arguments: ["clang", sourceFilename, "-include", contextFilename],
},
]
const initialFileState = {
".clang-format": defaultClangFormat,
}
initialFileState[sourceFilename] = initialScratchState.source_code
initialFileState[contextFilename] = initialScratchState.context
const _lsClient = new LanguageServerClient({
transport: new ClangdStdioTransportModule({
compileCommands,
initialFileState,
useSmallBinary: false,
}),
rootUri: "file:///",
workspaceFolders: null,
documentUri: null,
languageId,
})
const [sourceLsExtension, _saveSource] = languageServerWithTransport({
client: _lsClient,
transport: null,
rootUri: "file:///",
workspaceFolders: null,
documentUri: `file:///${sourceFilename}`,
languageId,
})
const [contextLsExtension, _saveContext] = languageServerWithTransport({
client: _lsClient,
transport: null,
rootUri: "file:///",
workspaceFolders: null,
documentUri: `file:///${contextFilename}`,
languageId,
})
// TODO: return the codemirror extensions instead of hotpatching them in?
// Given the async nature of the extension being ready, it'd require updating the Codemirror
// component to support inserting extensions when the extension prop changes
sourceEditor.current?.dispatch({ effects: StateEffect.appendConfig.of(sourceLsExtension) })
contextEditor.current?.dispatch({ effects: StateEffect.appendConfig.of(contextLsExtension) })
setSaveSource(() => _saveSource)
setSaveContext(() => _saveContext)
return () => {
_lsClient.exit()
}
}, [ClangdStdioTransportModule, initialScratchState, defaultClangFormat, sourceEditor, contextEditor])
const saveSourceRet = () => {
if (saveSource)
saveSource(scratch.source_code)
}
const saveContextRet = () => {
if (saveContext)
saveContext(scratch.context)
}
return [saveSourceRet, saveContextRet]
}

View File

@@ -42,6 +42,7 @@ export interface TerseScratch {
last_updated: string
compiler: string
platform: string
language: string
score: number // -1 = doesn't compile
max_score: number
project: string

View File

@@ -0,0 +1,659 @@
/* Originally adapted from https://github.com/FurqanSoftware/codemirror-languageserver */
import type { ClangdStdioTransport } from "@clangd-wasm/clangd-wasm"
import { autocompletion } from "@codemirror/autocomplete"
import type {
Completion,
CompletionContext,
CompletionResult,
} from "@codemirror/autocomplete"
import { setDiagnostics } from "@codemirror/lint"
import { Extension, Facet } from "@codemirror/state"
import type { Text } from "@codemirror/state"
import { EditorView, ViewPlugin, Tooltip, hoverTooltip, keymap } from "@codemirror/view"
import type { ViewUpdate, PluginValue } from "@codemirror/view"
import {
RequestManager,
Client,
} from "@open-rpc/client-js"
import { Transport } from "@open-rpc/client-js/build/transports/Transport"
import {
DiagnosticSeverity,
CompletionItemKind,
CompletionTriggerKind,
} from "vscode-languageserver-protocol"
import type * as LSP from "vscode-languageserver-protocol"
const timeout = 10000
const changesDelay = 500
const CompletionItemKindMap = Object.fromEntries(
Object.entries(CompletionItemKind).map(([key, value]) => [value, key])
) as Record<CompletionItemKind, string>
const useLast = (values: readonly any[]) => values.reduce((_, v) => v, "")
const client = Facet.define<LanguageServerClient, LanguageServerClient>({ combine: useLast })
const documentUri = Facet.define<string, string>({ combine: useLast })
const languageId = Facet.define<string, string>({ combine: useLast })
// https://microsoft.github.io/language-server-protocol/specifications/specification-current/
// Client to server then server to client
interface LSPRequestMap {
initialize: [LSP.InitializeParams, LSP.InitializeResult]
"textDocument/hover": [LSP.HoverParams, LSP.Hover]
"textDocument/completion": [
LSP.CompletionParams,
LSP.CompletionItem[] | LSP.CompletionList | null,
]
"textDocument/formatting": [LSP.DocumentFormattingParams, LSP.TextEdit[] | null]
"shutdown": [null, null]
}
// Client to server
interface LSPNotifyMap {
initialized: LSP.InitializedParams
"textDocument/didChange": LSP.DidChangeTextDocumentParams
"textDocument/didOpen": LSP.DidOpenTextDocumentParams
"textDocument/didSave": LSP.DidSaveTextDocumentParams
"exit": null
}
// Server to client
interface LSPEventMap {
"textDocument/publishDiagnostics": LSP.PublishDiagnosticsParams
}
type Notification = {
[key in keyof LSPEventMap]: {
jsonrpc: "2.0"
id?: null | undefined
method: key
params: LSPEventMap[key]
};
}[keyof LSPEventMap];
class LanguageServerClient {
private rootUri: string
private workspaceFolders: LSP.WorkspaceFolder[]
private autoClose?: boolean
private transport: Transport
private requestManager: RequestManager
private client: Client
public ready: boolean
public capabilities: LSP.ServerCapabilities<any>
private plugins: LanguageServerPlugin[]
public initializePromise: Promise<void>
constructor(options: LanguageServerClientOptions) {
this.rootUri = options.rootUri
this.workspaceFolders = options.workspaceFolders
this.autoClose = options.autoClose
this.plugins = []
this.transport = options.transport
this.requestManager = new RequestManager([this.transport])
this.client = new Client(this.requestManager)
this.client.onNotification(data => {
this.processNotification(data as any)
})
this.initializePromise = this.initialize()
}
async initialize() {
const { capabilities } = await this.request("initialize", {
capabilities: {
textDocument: {
hover: {
dynamicRegistration: true,
contentFormat: ["plaintext", "markdown"],
},
moniker: {},
synchronization: {
dynamicRegistration: true,
willSave: false,
didSave: true,
willSaveWaitUntil: false,
},
completion: {
dynamicRegistration: true,
completionItem: {
snippetSupport: false,
commitCharactersSupport: true,
documentationFormat: ["plaintext", "markdown"],
deprecatedSupport: false,
preselectSupport: false,
},
contextSupport: false,
},
signatureHelp: {
dynamicRegistration: true,
signatureInformation: {
documentationFormat: ["plaintext", "markdown"],
},
},
declaration: {
dynamicRegistration: true,
linkSupport: true,
},
definition: {
dynamicRegistration: true,
linkSupport: true,
},
typeDefinition: {
dynamicRegistration: true,
linkSupport: true,
},
implementation: {
dynamicRegistration: true,
linkSupport: true,
},
formatting: {
dynamicRegistration: true,
},
},
workspace: {
didChangeConfiguration: {
dynamicRegistration: true,
},
},
},
initializationOptions: null,
processId: null,
rootUri: this.rootUri,
workspaceFolders: this.workspaceFolders,
}, timeout * 3)
this.capabilities = capabilities
this.notify("initialized", {})
this.ready = true
}
async exit() {
await this.request("shutdown", null, timeout)
return this.notify("exit", null)
}
close() {
this.client.close()
}
textDocumentDidOpen(params: LSP.DidOpenTextDocumentParams) {
return this.notify("textDocument/didOpen", params)
}
textDocumentDidChange(params: LSP.DidChangeTextDocumentParams) {
return this.notify("textDocument/didChange", params)
}
async textDocumentHover(params: LSP.HoverParams) {
return await this.request("textDocument/hover", params, timeout)
}
async textDocumentCompletion(params: LSP.CompletionParams) {
return await this.request("textDocument/completion", params, timeout)
}
async textDocumentFormatting(params: LSP.DocumentFormattingParams) {
return await this.request("textDocument/formatting", params, timeout)
}
async textDocumentDidSave(params: LSP.DidSaveTextDocumentParams) {
return this.notify("textDocument/didSave", params)
}
transportWriteFile(filename: string, contents: string) {
const transport = <ClangdStdioTransport> this.transport
transport.module?.FS?.writeFile(filename, contents)
}
attachPlugin(plugin: LanguageServerPlugin) {
this.plugins.push(plugin)
}
detachPlugin(plugin: LanguageServerPlugin) {
const i = this.plugins.indexOf(plugin)
if (i === -1) return
this.plugins.splice(i, 1)
if (this.autoClose) this.close()
}
private request<K extends keyof LSPRequestMap>(
method: K,
params: LSPRequestMap[K][0],
timeout: number
): Promise<LSPRequestMap[K][1]> {
return this.client.request({ method, params }, timeout)
}
private notify<K extends keyof LSPNotifyMap>(
method: K,
params: LSPNotifyMap[K]
): Promise<LSPNotifyMap[K]> {
return this.client.notify({ method, params })
}
private processNotification(notification: Notification) {
for (const plugin of this.plugins)
plugin.processNotification(notification)
}
}
class LanguageServerPlugin implements PluginValue {
public client: LanguageServerClient
private documentUri: string
private languageId: string
private documentVersion: number
private changesTimeout: number
constructor(private view: EditorView) {
this.client = this.view.state.facet(client)
this.documentUri = this.view.state.facet(documentUri)
this.languageId = this.view.state.facet(languageId)
this.documentVersion = 0
this.changesTimeout = 0
this.client.attachPlugin(this)
this.initialize({
documentText: this.view.state.doc.toString(),
})
}
update({ docChanged }: ViewUpdate) {
if (!docChanged) return
if (this.changesTimeout) clearTimeout(this.changesTimeout)
this.changesTimeout = self.setTimeout(() => {
this.sendChange({
documentText: this.view.state.doc.toString(),
})
}, changesDelay)
}
async save(contents: string) {
this.client.transportWriteFile(this.documentUri.split("file://")[1], contents)
await this.client.textDocumentDidSave({
textDocument: {
uri: this.documentUri,
},
text: contents,
})
}
destroy() {
this.client.detachPlugin(this)
}
async initialize({ documentText }: { documentText: string }) {
if (this.client.initializePromise) {
await this.client.initializePromise
}
this.client.textDocumentDidOpen({
textDocument: {
uri: this.documentUri,
languageId: this.languageId,
text: documentText,
version: this.documentVersion,
},
})
}
async sendChange({ documentText }: { documentText: string }) {
if (!this.client.ready) return
try {
await this.client.textDocumentDidChange({
textDocument: {
uri: this.documentUri,
version: this.documentVersion++,
},
contentChanges: [{ text: documentText }],
})
} catch (e) {
console.error(e)
}
}
requestDiagnostics(view: EditorView) {
this.sendChange({ documentText: view.state.doc.toString() })
}
async requestHoverTooltip(
view: EditorView,
{ line, character }: { line: number, character: number }
): Promise<Tooltip | null> {
if (!this.client.ready || !this.client.capabilities.hoverProvider) return null
this.sendChange({ documentText: view.state.doc.toString() })
const result = await this.client.textDocumentHover({
textDocument: { uri: this.documentUri },
position: { line, character },
})
if (!result) return null
const { contents, range } = result
let pos = posToOffset(view.state.doc, { line, character })
let end: number
if (range) {
pos = posToOffset(view.state.doc, range.start)
end = posToOffset(view.state.doc, range.end)
}
if (pos === null) return null
const dom = document.createElement("div")
dom.classList.add("documentation")
dom.innerHTML = formatContents(contents).join("<br/>")
return { pos, end, create: _view => ({ dom }), above: true }
}
async requestCompletion(
context: CompletionContext,
{ line, character }: { line: number, character: number },
{
triggerKind,
triggerCharacter,
}: {
triggerKind: CompletionTriggerKind
triggerCharacter: string | undefined
}
): Promise<CompletionResult | null> {
if (!this.client.ready || !this.client.capabilities.completionProvider) return null
this.sendChange({
documentText: context.state.doc.toString(),
})
const result = await this.client.textDocumentCompletion({
textDocument: { uri: this.documentUri },
position: { line, character },
context: {
triggerKind,
triggerCharacter,
},
})
if (!result) return null
const items = "items" in result ? result.items : result
let options = items.map(
({
detail,
label,
kind,
textEdit,
documentation,
sortText,
filterText,
}) => {
const completion: Completion & {
filterText: string
sortText?: string
apply: string
} = {
label,
detail,
apply: textEdit?.newText ?? label,
type: kind && CompletionItemKindMap[kind].toLowerCase(),
sortText: sortText ?? label,
filterText: filterText ?? label,
}
if (documentation) {
completion.info = formatContents(documentation).join("\n")
}
return completion
}
)
const [, match] = prefixMatch(options)
const token = context.matchBefore(match)
let { pos } = context
if (token) {
pos = token.from
const word = token.text.toLowerCase()
if (/^\w+$/.test(word)) {
options = options
.filter(({ filterText }) =>
filterText.toLowerCase().startsWith(word)
)
.sort(({ apply: a }, { apply: b }) => {
switch (true) {
case a.startsWith(token.text) &&
!b.startsWith(token.text):
return -1
case !a.startsWith(token.text) &&
b.startsWith(token.text):
return 1
}
return 0
})
}
}
return {
from: pos,
options,
}
}
async requestFormat() {
return await this.client.textDocumentFormatting({
textDocument: {
uri: this.documentUri,
},
// We need this to be protocol-compilant, but clangd *entirely* ignores it.
// For control over formatting options, see .clang-format
options: {
tabSize: 4,
insertSpaces: true,
},
})
}
processNotification(notification: Notification) {
try {
switch (notification.method) {
case "textDocument/publishDiagnostics":
this.processDiagnostics(notification.params)
}
} catch (error) {
console.error(error)
}
}
processDiagnostics(params: LSP.PublishDiagnosticsParams) {
if (params.uri !== this.documentUri) return
const diagnostics = params.diagnostics
.map(({ range, message, severity }) => ({
from: posToOffset(this.view.state.doc, range.start),
to: posToOffset(this.view.state.doc, range.end),
severity: ({
[DiagnosticSeverity.Error]: "error",
[DiagnosticSeverity.Warning]: "warning",
[DiagnosticSeverity.Information]: "info",
[DiagnosticSeverity.Hint]: "info",
} as const)[severity],
message,
}))
.filter(({ from, to }) => from !== null && to !== null && from !== undefined && to !== undefined)
.sort((a, b) => {
switch (true) {
case a.from < b.from:
return -1
case a.from > b.from:
return 1
}
return 0
})
this.view.dispatch(setDiagnostics(this.view.state, diagnostics))
}
}
interface LanguageServerBaseOptions {
rootUri: string | null
workspaceFolders: LSP.WorkspaceFolder[] | null
documentUri: string
languageId: string
}
interface LanguageServerClientOptions extends LanguageServerBaseOptions {
transport: Transport
autoClose?: boolean
}
interface LanguageServerOptions extends LanguageServerClientOptions {
client?: LanguageServerClient
}
function languageServerWithTransport(options: LanguageServerOptions): [Extension[], (string) => Promise<void>]{
let plugin: LanguageServerPlugin | null = null
const extension = [
client.of(options.client || new LanguageServerClient({ ...options, autoClose: true })),
documentUri.of(options.documentUri),
languageId.of(options.languageId),
ViewPlugin.define(view => (plugin = new LanguageServerPlugin(view))),
hoverTooltip(
(view, pos) =>
plugin?.requestHoverTooltip(
view,
offsetToPos(view.state.doc, pos)
) ?? null
),
autocompletion({
override: [
async context => {
if (plugin == null) return null
const { state, pos, explicit } = context
const line = state.doc.lineAt(pos)
let trigKind: CompletionTriggerKind =
CompletionTriggerKind.Invoked
let trigChar: string | undefined
if (
!explicit &&
plugin.client.capabilities?.completionProvider?.triggerCharacters?.includes(
line.text[pos - line.from - 1]
)
) {
trigKind = CompletionTriggerKind.TriggerCharacter
trigChar = line.text[pos - line.from - 1]
}
if (
trigKind === CompletionTriggerKind.Invoked &&
!context.matchBefore(/\w+$/)
) {
return null
}
return await plugin.requestCompletion(
context,
offsetToPos(state.doc, pos),
{
triggerKind: trigKind,
triggerCharacter: trigChar,
}
)
},
],
}),
keymap.of([
{ key: "Alt-Shift-f", run: target => {
(async () => {
const formattingEdits = await plugin.requestFormat()
if (!formattingEdits) return
const changes = formattingEdits.map(change => {
return {
from: posToOffset(target.state.doc, change.range.start),
to: posToOffset(target.state.doc, change.range.end),
insert: change.newText,
}
})
target.dispatch({ changes })
})()
return true
} },
]),
]
const saveDocument = async (contents: string) => {
if (!plugin) return
await plugin.save(contents)
}
return [extension, saveDocument]
}
function posToOffset(doc: Text, pos: { line: number, character: number }) {
if (pos.line >= doc.lines) return
const offset = doc.line(pos.line + 1).from + pos.character
if (offset > doc.length) return
return offset
}
function offsetToPos(doc: Text, offset: number) {
const line = doc.lineAt(offset)
return {
line: line.number - 1,
character: offset - line.from,
}
}
function formatContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string[] {
if (Array.isArray(contents)) {
return contents.flatMap(formatContentEntry)
} else {
return formatContentEntry(contents)
}
}
function formatContentEntry(entry: LSP.MarkupContent | LSP.MarkedString): string[] {
if (typeof entry === "string") {
return entry.split("\n")
} else {
return formatContentEntry(entry.value)
}
}
function toSet(chars: Set<string>) {
let preamble = ""
let flat = Array.from(chars).join("")
const words = /\w/.test(flat)
if (words) {
preamble += "\\w"
flat = flat.replace(/\w/g, "")
}
return `[${preamble}${flat.replace(/[^\w\s]/g, "\\$&")}]`
}
function prefixMatch(options: Completion[]) {
const first = new Set<string>()
const rest = new Set<string>()
for (const { apply } of options) {
const applyString = apply as string
const initial = applyString.at(0)
const restStr = applyString.substring(1)
first.add(initial)
for (const char of restStr) {
rest.add(char)
}
}
const source = toSet(first) + toSet(rest) + "*$"
return [new RegExp("^" + source), new RegExp(source)]
}
export { LanguageServerClient, languageServerWithTransport }

View File

@@ -1,5 +1,3 @@
import { mutate } from "swr"
import { ResponseError } from "./api"
const GITHUB_CLIENT_ID = process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID
@@ -9,20 +7,9 @@ export function isGitHubLoginSupported(): boolean {
}
// https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps
export function showGitHubLoginWindow(popup: boolean, scope: string) {
export function showGitHubLoginWindow(scope: string) {
const url = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&scope=${encodeURIComponent(scope)}`
const win = popup && window.open(url, "Sign in with GitHub", "popup,width=520,height=520,resizable,status")
if (win) {
win.addEventListener("message", event => {
if (event.data?.source === "decomp_me_login") {
console.info("Got new user from popup", event.data.user)
mutate("/user", event.data.user)
}
})
} else {
window.location.href = url
}
window.location.href = url
}
export async function requestMissingScopes<T>(makeRequest: () => Promise<T>): Promise<T> {
@@ -33,7 +20,7 @@ export async function requestMissingScopes<T>(makeRequest: () => Promise<T>): Pr
const scope = error.json.detail
console.warn("Missing scopes", scope)
showGitHubLoginWindow(true, scope)
showGitHubLoginWindow(scope)
throw new Error("Accept permissions and retry")
} else {

View File

@@ -7,6 +7,7 @@ const codeFontSize = createPersistedState("codeFontSize")
const monospaceFont = createPersistedState("monospaceFont")
const codeLineHeight = createPersistedState("codeLineHeight")
const codeColorScheme = createPersistedState("codeColorScheme")
const languageServerEnabled = createPersistedState("languageServerEnabled")
export const useTheme = () => theme("auto")
export const useAutoRecompileSetting = () => autoRecompile(true)
@@ -15,6 +16,7 @@ export const useCodeFontSize = () => codeFontSize(11)
export const useMonospaceFont = () => monospaceFont(undefined)
export const useCodeLineHeight = () => codeLineHeight(1.5)
export const useCodeColorScheme = () => codeColorScheme("Frog Dark")
export const useLanguageServerEnabled = () => languageServerEnabled(false)
export function useIsSiteThemeDark() {
const [theme] = useTheme()

View File

@@ -111,7 +111,7 @@ export default function NewProjectPage() {
disabled={!isSignedIn}
>
Create project
</AsyncButton> : <GitHubLoginButton popup label="Sign in to create projects" />}
</AsyncButton> : <GitHubLoginButton label="Sign in to create projects" />}
</div>
</div>
</main>

View File

@@ -1013,6 +1013,13 @@
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
"@clangd-wasm/clangd-wasm@15.0.7-dev5":
version "15.0.7-dev5"
resolved "https://registry.yarnpkg.com/@clangd-wasm/clangd-wasm/-/clangd-wasm-15.0.7-dev5.tgz#6787f95ae28574c0f5addb6ca44177f0dd358a5c"
integrity sha512-HwzZ0EgSayCgjgk9WRVjiurrqo2Tc7iJR1s1PDx718/kOSPgnc1JyIMH/0EazLDfB2pOPDewnxNwdCSDnDhz+w==
dependencies:
"@open-rpc/client-js" "^1.8.1"
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.6.0":
version "6.6.0"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.6.0.tgz#9c0ea57792b405a391599bd80acae19b8c4c6ff5"
@@ -1330,6 +1337,16 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@open-rpc/client-js@^1.8.1":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@open-rpc/client-js/-/client-js-1.8.1.tgz#73b5a5bf237f24b14c3c89205b1fca3aea213213"
integrity sha512-vV+Hetl688nY/oWI9IFY0iKDrWuLdYhf7OIKI6U1DcnJV7r4gAgwRJjEr1QVYszUc0gjkHoQJzqevmXMGLyA0g==
dependencies:
isomorphic-fetch "^3.0.0"
isomorphic-ws "^5.0.0"
strict-event-emitter-types "^2.0.0"
ws "^7.0.0"
"@pkgr/utils@^2.3.1":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.0.tgz#b6373d2504aedaf2fc7cdf2d13ab1f48fa5f12d5"
@@ -2113,6 +2130,16 @@ ast-types-flow@^0.0.7:
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==
ast-types@0.9.6:
version "0.9.6"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.6.tgz#102c9e9e9005d3e7e3829bf0c4fa24ee862ee9b9"
integrity sha512-qEdtR2UH78yyHX/AUNfXmJTlM48XoFZKBdwi1nzkI1mJL21cmbu0cvjxjpkXJ5NENMq42H+hNs8VLJcqXLerBQ==
ast-types@^0.9.2:
version "0.9.14"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.14.tgz#d34ba5dffb9d15a44351fd2a9d82e4ab2838b5ba"
integrity sha512-Ebvx7/0lLboCdyEmAw/4GqwBeKIijPveXNiVGhCGCNxc7z26T5he7DC6ARxu8ByKuzUZZcLog+VP8GMyZrBzJw==
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@@ -3264,6 +3291,11 @@ espree@^9.5.1:
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.4.0"
esprima@~3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
integrity sha512-AWwVMNxwhN8+NIPQzAQZCm7RkLC4RbM3B1OobMuyp3i+w73X57KCKaVIxaRZb+DYCojq7rspo+fmuQfAboyhFg==
esquery@^1.4.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
@@ -4153,6 +4185,19 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
isomorphic-fetch@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4"
integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==
dependencies:
node-fetch "^2.6.1"
whatwg-fetch "^3.4.1"
isomorphic-ws@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf"
integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==
jake@^10.8.5:
version "10.8.5"
resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46"
@@ -4238,7 +4283,7 @@ json-stable-stringify-without-jsonify@^1.0.1:
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^1.0.2:
json5@^1.0.1, json5@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
@@ -4327,6 +4372,15 @@ loader-runner@^4.2.0:
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1"
integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==
loader-utils@^1.0.3:
version "1.4.2"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3"
integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
json5 "^1.0.1"
loader-utils@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c"
@@ -4670,6 +4724,13 @@ node-addon-api@^6.1.0:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76"
integrity sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==
node-fetch@^2.6.1:
version "2.6.11"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25"
integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==
dependencies:
whatwg-url "^5.0.0"
node-releases@^2.0.8:
version "2.0.10"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f"
@@ -5325,6 +5386,11 @@ pretty-bytes@^5.3.0, pretty-bytes@^5.4.1:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
private@~0.1.5:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
prop-types@^15.7.1, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
@@ -5494,6 +5560,16 @@ readdirp@~3.6.0:
dependencies:
picomatch "^2.2.1"
recast@^0.11.17:
version "0.11.23"
resolved "https://registry.yarnpkg.com/recast/-/recast-0.11.23.tgz#451fd3004ab1e4df9b4e4b66376b2a21912462d3"
integrity sha512-+nixG+3NugceyR8O1bLU45qs84JgI3+8EauyRZafLgC9XbdAOIVgwV1Pe2da0YzGo62KzWoZwUpVEQf6qNAXWA==
dependencies:
ast-types "0.9.6"
esprima "~3.1.0"
private "~0.1.5"
source-map "~0.5.0"
redent@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
@@ -5858,6 +5934,11 @@ source-map@^0.8.0-beta.0:
dependencies:
whatwg-url "^7.0.0"
source-map@~0.5.0:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
@@ -5901,6 +5982,11 @@ streamsearch@^1.1.0:
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
strict-event-emitter-types@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f"
integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA==
string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
@@ -6372,6 +6458,11 @@ tr46@^1.0.1:
dependencies:
punycode "^2.1.0"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
trim-newlines@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
@@ -6462,6 +6553,15 @@ typescript@^5.0.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
umd-compat-loader@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/umd-compat-loader/-/umd-compat-loader-2.1.2.tgz#abf89be1591940a236cf8fa87f88d6d6f5a8da35"
integrity sha512-RkTlsfrCxUISWqiTtYFFJank7b2Hhl4V2pc29nl0xOEGvvuVkpy1xnufhXfTituxgpW0HSrDk0JHlvPYZxEXKQ==
dependencies:
ast-types "^0.9.2"
loader-utils "^1.0.3"
recast "^0.11.17"
unbox-primitive@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"
@@ -6569,6 +6669,24 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0"
vscode-jsonrpc@8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94"
integrity sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==
vscode-languageserver-protocol@^3.17.3:
version "3.17.3"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz#6d0d54da093f0c0ee3060b81612cce0f11060d57"
integrity sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==
dependencies:
vscode-jsonrpc "8.1.0"
vscode-languageserver-types "3.17.3"
vscode-languageserver-types@3.17.3:
version "3.17.3"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64"
integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==
w3c-keyname@^2.2.4:
version "2.2.6"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f"
@@ -6589,6 +6707,11 @@ watchpack@^2.4.0:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webidl-conversions@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -6652,6 +6775,19 @@ webpack@^5.81.0:
watchpack "^2.4.0"
webpack-sources "^3.2.3"
whatwg-fetch@^3.4.1:
version "3.6.2"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
whatwg-url@^7.0.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
@@ -6895,7 +7031,7 @@ write-file-atomic@^5.0.1:
imurmurhash "^0.1.4"
signal-exit "^4.0.1"
ws@^7.3.1:
ws@^7.0.0, ws@^7.3.1:
version "7.5.9"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==