refactor(editor): extract language server configuration

Diagnostics for python, go, deno and shell are decided by which server runs
and with which initialization options, so headless linting has to build its
clients from the same descriptors. Move them to lint/lspLanguageConfig.ts;
the editor keeps owning the websocket lifecycle and reconnection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-07-20 12:59:37 +02:00
parent 11b58cacb5
commit f5b9dc7f5c
2 changed files with 184 additions and 141 deletions
+14 -141
View File
@@ -7,7 +7,6 @@
<script lang="ts">
import { BROWSER } from 'esm-env'
import { buildWsUrl } from '$lib/wsUrl'
import { sendUserToast } from '$lib/toast'
import { createEventDispatcher, onDestroy, onMount, untrack } from 'svelte'
@@ -108,6 +107,7 @@
} from './lint/typescriptExtraLibs'
import { createWindmillAta, genAtaRoot } from './lint/typescriptAta'
import { readModelMarkers } from './lint/headlessLint'
import { buildDenoImportMap, lspServersFor } from './lint/lspLanguageConfig'
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
import type { Selection } from 'monaco-editor'
import { canHavePreprocessor, getPreprocessorModuleCode } from '$lib/script_helpers'
@@ -1139,152 +1139,25 @@
}
}
let encodedImportMap = ''
if (useWebsockets) {
let denoImportMap: string | undefined = undefined
if (lang == 'typescript' && scriptLang === 'deno') {
ata = undefined
let root = await genAtaRoot($workspaceStore ?? '')
const importMap = {
imports: {
'file:///': root + '/'
}
}
if (filePath && filePath.split('/').length > 2) {
let path_splitted = filePath.split('/')
for (let c = 0; c < path_splitted.length; c++) {
let key = 'file://./'
for (let i = 0; i < c; i++) {
key += '../'
}
let url = path_splitted.slice(0, -c - 1).join('/')
let ending = c == path_splitted.length - 1 ? '' : '/'
importMap['imports'][key] = `${root}/${url}${ending}`
}
}
encodedImportMap = 'data:text/plain;base64,' + btoa(JSON.stringify(importMap))
await connectToLanguageServer(
buildWsUrl('/ws/deno'),
'deno',
{
certificateStores: null,
enablePaths: [],
config: null,
importMap: encodedImportMap,
internalDebug: false,
lint: false,
path: null,
tlsCertificate: null,
unsafelyIgnoreCertificateErrors: null,
unstable: true,
enable: true,
codeLens: {
implementations: true,
references: true,
referencesAllFunction: false
},
suggest: {
autoImports: true,
completeFunctionCalls: false,
names: true,
paths: true,
imports: {
autoDiscover: true,
hosts: {
'https://deno.land': true
}
}
}
},
() => {
return [
{
enable: true
}
]
}
)
} else if (lang === 'python') {
await connectToLanguageServer(
buildWsUrl('/ws/pyright'),
'pyright',
{},
(params, token, next) => {
if (params.items.find((x) => x.section === 'python')) {
return [
{
analysis: {
useLibraryCodeForTypes: true,
autoImportCompletions: true,
diagnosticSeverityOverrides: { reportMissingImports: 'none' },
typeCheckingMode: 'basic'
}
}
]
}
if (params.items.find((x) => x.section === 'python.analysis')) {
return [
{
useLibraryCodeForTypes: true,
autoImportCompletions: true,
diagnosticSeverityOverrides: { reportMissingImports: 'none' },
typeCheckingMode: 'basic'
}
]
}
return next(params, token)
}
)
denoImportMap = buildDenoImportMap(await genAtaRoot($workspaceStore ?? ''), filePath)
}
connectToLanguageServer(buildWsUrl('/ws/ruff'), 'ruff', {}, undefined)
} else if (lang === 'go') {
connectToLanguageServer(
buildWsUrl('/ws/go'),
'go',
{
'build.allowImplicitNetworkAccess': true
},
undefined
)
} else if (lang === 'shell') {
connectToLanguageServer(
buildWsUrl('/ws/diagnostic'),
'shellcheck',
{
linters: {
shellcheck: {
command: 'shellcheck',
debounce: 100,
args: ['--format=gcc', '-'],
offsetLine: 0,
offsetColumn: 0,
sourceName: 'shellcheck',
formatLines: 1,
formatPattern: [
'^[^:]+:(\\d+):(\\d+):\\s+([^:]+):\\s+(.*)$',
{
line: 1,
column: 2,
message: 4,
security: 3
}
],
securities: {
error: 'error',
warning: 'warning',
note: 'info'
}
}
},
filetypes: {
shell: 'shellcheck'
}
},
undefined
)
} else {
const servers = lspServersFor({ editorLang: lang, scriptLang, denoImportMap })
if (servers.length === 0) {
closeWebsockets()
}
for (const server of servers) {
await connectToLanguageServer(
server.url,
server.name,
server.initOptions,
server.middleware
)
}
websocketInterval && clearInterval(websocketInterval)
websocketInterval = setInterval(() => {
@@ -0,0 +1,170 @@
import { buildWsUrl } from '$lib/wsUrl'
// Which language servers back a given language, and with which initialization options.
// Diagnostics are decided by the server plus these options, so a headless linter and a
// mounted editor only agree as long as both build their clients from here.
export interface LspServerConfig {
name: string
url: string
initOptions: any
middleware?: (params: any, token: any, next: any) => any
}
export function lspLanguagesFor(editorLang: string, scriptLang: string | undefined): boolean {
return (
(editorLang === 'typescript' && scriptLang === 'deno') ||
editorLang === 'python' ||
editorLang === 'go' ||
editorLang === 'shell'
)
}
/** Deno resolves relative imports through an import map rooted at the tokened raw endpoint. */
export function buildDenoImportMap(root: string, filePath: string | undefined): string {
const importMap: { imports: Record<string, string> } = {
imports: {
'file:///': root + '/'
}
}
if (filePath && filePath.split('/').length > 2) {
const path_splitted = filePath.split('/')
for (let c = 0; c < path_splitted.length; c++) {
let key = 'file://./'
for (let i = 0; i < c; i++) {
key += '../'
}
const url = path_splitted.slice(0, -c - 1).join('/')
const ending = c == path_splitted.length - 1 ? '' : '/'
importMap['imports'][key] = `${root}/${url}${ending}`
}
}
return 'data:text/plain;base64,' + btoa(JSON.stringify(importMap))
}
function denoServer(encodedImportMap: string): LspServerConfig {
return {
name: 'deno',
url: buildWsUrl('/ws/deno'),
initOptions: {
certificateStores: null,
enablePaths: [],
config: null,
importMap: encodedImportMap,
internalDebug: false,
lint: false,
path: null,
tlsCertificate: null,
unsafelyIgnoreCertificateErrors: null,
unstable: true,
enable: true,
codeLens: {
implementations: true,
references: true,
referencesAllFunction: false
},
suggest: {
autoImports: true,
completeFunctionCalls: false,
names: true,
paths: true,
imports: {
autoDiscover: true,
hosts: {
'https://deno.land': true
}
}
}
},
middleware: () => [{ enable: true }]
}
}
const PYTHON_ANALYSIS_SETTINGS = {
useLibraryCodeForTypes: true,
autoImportCompletions: true,
diagnosticSeverityOverrides: { reportMissingImports: 'none' },
typeCheckingMode: 'basic'
}
const pyrightServer: LspServerConfig = {
name: 'pyright',
url: buildWsUrl('/ws/pyright'),
initOptions: {},
middleware: (params, token, next) => {
if (params.items.find((x) => x.section === 'python')) {
return [{ analysis: PYTHON_ANALYSIS_SETTINGS }]
}
if (params.items.find((x) => x.section === 'python.analysis')) {
return [PYTHON_ANALYSIS_SETTINGS]
}
return next(params, token)
}
}
const ruffServer: LspServerConfig = {
name: 'ruff',
url: buildWsUrl('/ws/ruff'),
initOptions: {}
}
const goServer: LspServerConfig = {
name: 'go',
url: buildWsUrl('/ws/go'),
initOptions: { 'build.allowImplicitNetworkAccess': true }
}
const shellcheckServer: LspServerConfig = {
name: 'shellcheck',
url: buildWsUrl('/ws/diagnostic'),
initOptions: {
linters: {
shellcheck: {
command: 'shellcheck',
debounce: 100,
args: ['--format=gcc', '-'],
offsetLine: 0,
offsetColumn: 0,
sourceName: 'shellcheck',
formatLines: 1,
formatPattern: [
'^[^:]+:(\\d+):(\\d+):\\s+([^:]+):\\s+(.*)$',
{
line: 1,
column: 2,
message: 4,
security: 3
}
],
securities: {
error: 'error',
warning: 'warning',
note: 'info'
}
}
},
filetypes: {
shell: 'shellcheck'
}
}
}
export function lspServersFor(opts: {
editorLang: string
scriptLang: string | undefined
denoImportMap?: string
}): LspServerConfig[] {
if (opts.editorLang === 'typescript' && opts.scriptLang === 'deno') {
return [denoServer(opts.denoImportMap ?? '')]
}
if (opts.editorLang === 'python') {
return [pyrightServer, ruffServer]
}
if (opts.editorLang === 'go') {
return [goServer]
}
if (opts.editorLang === 'shell') {
return [shellcheckServer]
}
return []
}