Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Init and refresh tags file #32

Merged
merged 1 commit into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to the "alloglot" extension will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
This project adhere's to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [unreleased]

- Reload config and restart extension when config changes.
- Add `initTagsCommand` and `refreshTagsCommand` to `TagsConfig`.
- Add _Restart Alloglot_ command.

## [2.3.0] - 2024-02-14

- If there the user has no user-level or workspace-level alloglot settings, settings will be read from a file `.vscode/alloglot.json` if one exists.
Expand Down
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Most of the properties are optional, so you can make use of only the features th
"apiSearchUrl": "https://hoogle.haskell.org/?hoogle=${query}",
"tags": {
"file": ".tags",
"initTagsCommand": "ghc-tags -c",
"refreshTagsCommand": "ghc-tags -c",
"completionsProvider": true,
"definitionsProvider": true,
"importsProvider": {
Expand Down Expand Up @@ -119,7 +121,7 @@ export type LanguageConfig = {
/**
* A formatter command.
* Reads from STDIN and writes to STDOUT.
* `${file}` will be replaced with the path to the file.
* `${file}` will be replaced with the relative path to the file.
*/
formatCommand?: string

Expand Down Expand Up @@ -147,17 +149,28 @@ export type TagsConfig = {
file: string

/**
* Use the contents of this tags file to suggest completions.
* A command to generate the tags file.
*/
initTagsCommand?: string

/**
* A command to refresh the tags file when a file is saved.
* `${file}` will be replaced with the relative path to the file.
*/
refreshTagsCommand?: string

/**
* Indicates that this tags file should be used to suggest completions.
*/
completionsProvider?: boolean

/**
* Use the contents of this tags file to go to definitions.
* Indicates that this tags file should be used to go to definitions.
*/
definitionsProvider?: boolean

/**
* Use the contents of this tags file to suggest imports.
* Indicates that this tags file should be used to suggest imports for symbols.
*/
importsProvider?: ImportsProviderConfig
}
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "alloglot.command.restart",
"title": "Restart Alloglot"
},
{
"command": "alloglot.command.apisearch",
"title": "Go to API Search"
Expand Down
81 changes: 77 additions & 4 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import * as vscode from 'vscode'
import { readFileSync } from 'fs'

/**
* Extension configuration.
*/
Expand Down Expand Up @@ -26,7 +29,7 @@ export type LanguageConfig = {
/**
* A formatter command.
* Reads from STDIN and writes to STDOUT.
* `${file}` will be replaced with the path to the file.
* `${file}` will be replaced with the relative path to the file.
*/
formatCommand?: string

Expand Down Expand Up @@ -54,17 +57,28 @@ export type TagsConfig = {
file: string

/**
* Use the contents of this tags file to suggest completions.
* A command to generate the tags file.
*/
initTagsCommand?: string

/**
* A command to refresh the tags file when a file is saved.
* `${file}` will be replaced with the relative path to the file.
*/
refreshTagsCommand?: string

/**
* Indicates that this tags file should be used to suggest completions.
*/
completionsProvider?: boolean

/**
* Use the contents of this tags file to go to definitions.
* Indicates that this tags file should be used to go to definitions.
*/
definitionsProvider?: boolean

/**
* Use the contents of this tags file to suggest imports.
* Indicates that this tags file should be used to suggest imports for symbols.
*/
importsProvider?: ImportsProviderConfig
}
Expand Down Expand Up @@ -155,6 +169,62 @@ export type AnnotationsMapping = {
referenceCode?: Array<string>
}

export namespace Config {
export function create(): Config {
return sanitizeConfig(readSettings() || readFallback() || empty)
}

const empty: Config = { languages: [] }

function readFallback(): Config | undefined {
const workspaceFolders = vscode.workspace.workspaceFolders?.map(folder => folder.uri)
try {
if (workspaceFolders && workspaceFolders.length > 0) {
const fullPath = vscode.Uri.joinPath(workspaceFolders[0], alloglot.config.fallbackPath)
return JSON.parse(readFileSync(fullPath.path, 'utf-8'))
}
} catch (err) {
return undefined
}
}

function readSettings(): Config | undefined {
const languages = vscode.workspace.getConfiguration(alloglot.config.root).get<Array<LanguageConfig>>(alloglot.config.languages)
return languages && { languages }
}

function sanitizeConfig(config: Config): Config {
return {
languages: config.languages
.filter(lang => {
// make sure no fields are whitespace-only
// we mutate the original object because typescript doesn't have a `filterMap` function

lang.languageId = lang.languageId.trim()
lang.serverCommand = lang.serverCommand?.trim()
lang.formatCommand = lang.formatCommand?.trim()
lang.apiSearchUrl = lang.apiSearchUrl?.trim()

lang.annotations = lang.annotations?.filter(ann => {
ann.file = ann.file.trim()
return ann.file
})

if (lang.tags) {
lang.tags.file = lang.tags.file.trim()
lang.tags.initTagsCommand = lang.tags.initTagsCommand?.trim()
lang.tags.refreshTagsCommand = lang.tags.refreshTagsCommand?.trim()
if (!lang.tags?.importsProvider?.importLinePattern.trim()) lang.tags.importsProvider = undefined
if (!lang.tags?.importsProvider?.matchFromFilepath.trim()) lang.tags.importsProvider = undefined
if (!lang.tags.file) lang.tags = undefined
}

return lang.languageId
})
}
}
}

export namespace alloglot {
export const root = 'alloglot' as const

Expand All @@ -164,11 +234,14 @@ export namespace alloglot {

export namespace commands {
const root = `${alloglot.root}.command` as const
export const restart = `${root}.restart` as const
export const apiSearch = `${root}.apisearch` as const
export const suggestImports = `${root}.suggestimports` as const
}

export namespace config {
export const root = alloglot.root
export const fallbackPath = `.vscode/${root}.json` as const
export const languages = 'languages' as const
}
}
70 changes: 22 additions & 48 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,15 @@ import * as vscode from 'vscode'
import { makeAnnotations } from './annotations'
import { makeApiSearch } from './apisearch'
import { makeClient } from './client'
import { Config, LanguageConfig, alloglot } from './config'
import { Config, alloglot } from './config'
import { makeFormatter } from './formatter'
import { makeTags } from './tags'
import { readFileSync, writeFile, writeFileSync } from 'fs'

export function activate(context: vscode.ExtensionContext): void {
const settingsSection = vscode.workspace.getConfiguration(alloglot.root)
const alloglotWorkspaceLanguages = getWorkspaceConfig('.vscode/alloglot.json');
const alloglotVscodeLanguages = settingsSection.get<Array<LanguageConfig>>(alloglot.config.languages, [])
const alloglotLanguages = alloglotVscodeLanguages.length === 0 ? alloglotWorkspaceLanguages : alloglotVscodeLanguages;

const config: Config = {
languages: alloglotLanguages
.filter(lang => {
// make sure no fields are whitespace-only
// we mutate the original object because typescript doesn't have a `filterMap` function

lang.languageId = lang.languageId.trim()
lang.serverCommand = lang.serverCommand?.trim()
lang.formatCommand = lang.formatCommand?.trim()
lang.apiSearchUrl = lang.apiSearchUrl?.trim()

lang.annotations = lang.annotations?.filter(ann => {
ann.file = ann.file.trim()
return ann.file
})

if (lang.tags) {
lang.tags.file = lang.tags.file.trim()
if (!lang.tags?.importsProvider?.importLinePattern.trim()) lang.tags.importsProvider = undefined
if (!lang.tags?.importsProvider?.matchFromFilepath.trim()) lang.tags.importsProvider = undefined
if (!lang.tags.file) lang.tags = undefined
}
let globalContext: vscode.ExtensionContext | undefined

return lang.languageId
})
}
export function activate(context: vscode.ExtensionContext): void {
globalContext = context
const config = Config.create()

context.subscriptions.push(
// Make a single API search command because VSCode can't dynamically create commands.
Expand All @@ -51,23 +23,25 @@ export function activate(context: vscode.ExtensionContext): void {
// ...dynamically create LSP clients
...config.languages.map(makeClient),
// ...and dynamically create completions, definitions, and code actions providers.
...config.languages.map(makeTags)
...config.languages.map(makeTags),
// restart extension when config changes
vscode.workspace.onDidChangeConfiguration(ev => {
if (ev.affectsConfiguration(alloglot.config.root)) restart(context)
}),
// user command to restart extension
vscode.commands.registerCommand(alloglot.commands.restart, () => restart(context)),
)
}

function getWorkspaceConfig(workspaceConfigPath: string): LanguageConfig[] {
const workspaceFolders = vscode.workspace.workspaceFolders?.map(folder => folder.uri)
try {
if (workspaceFolders && workspaceFolders.length > 0)
{
const fullPath = vscode.Uri.joinPath(workspaceFolders[0], workspaceConfigPath);
return JSON.parse(readFileSync(fullPath.path,'utf-8')).languages;
} else {
return []
}
} catch (err){
return []
}
export function deactivate() {
globalContext && disposeAll(globalContext)
}

function disposeAll(context: vscode.ExtensionContext) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway we can make this kill the spawned processes here? So we don't need to do that trap stuff?

Copy link
Contributor Author

@friedbrice friedbrice Mar 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VS Code does kill the spawned language server. Our problem is slightly different, though. Our "language server" is a script that forks ghciwatch and starts the real language server, static-ls. VS Code will kill static-ls without issue. The trap causes ghciwatch to die whenever static-ls dies. Without trap, VS Code will kill static-ls, but ghciwatch will be left a zombie.

context.subscriptions.forEach(sub => sub.dispose())
}

export function deactivate() { }
function restart(context: vscode.ExtensionContext) {
disposeAll(context)
activate(context)
}
Loading
Loading