mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-19 08:48:21 +03:00
Multi-IDE Features for 2.9 (#19040)
* Implement dar unpacking * Implement packageless IDE * Hot-reloading logic for daml.yaml, multi-package.yaml, *.dar * Implement initial error recovery logic * Switch logging to log levels Replace window reload with LanguageServer restart * Forward args from multi-ide to sub-ides * Change unpacked dar paths to be the unit-id. Update unpacking logic to shutdown previous IDEs * Remove broken experimental flag * Refactor ide restart logic to not lose event handlers * Log subIDE errors to debug logger live * Windows fixes * First review fixes batch * Use newtypes for many FilePaths * Address Dylan's comments * Refactor how SubIDEs are passed around, reduce times it is dropped * Update diagnostic
This commit is contained in:
parent
d80bd95cd6
commit
edea1e6253
@ -551,6 +551,7 @@ exports_files(["stack.exe"], visibility = ["//visibility:public"])
|
||||
"semigroupoids",
|
||||
"semver",
|
||||
"silently",
|
||||
"some",
|
||||
"sorted-list",
|
||||
"split",
|
||||
"stache",
|
||||
|
@ -15,7 +15,8 @@
|
||||
"onLanguage:daml",
|
||||
"onCommand:daml.openDamlDocs",
|
||||
"onCommand:daml.resetTelemetryConsent",
|
||||
"onCommand:daml.showResource"
|
||||
"onCommand:daml.showResource",
|
||||
"workspaceContains:daml.yaml"
|
||||
],
|
||||
"main": "./out/src/extension",
|
||||
"contributes": {
|
||||
@ -78,15 +79,16 @@
|
||||
"type": "object",
|
||||
"title": "Daml Studio configuration",
|
||||
"properties": {
|
||||
"daml.debug": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable debug logging in the Daml Language Server."
|
||||
},
|
||||
"daml.experimental": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Enable experimental features in the IDE, this may break things"
|
||||
"daml.logLevel": {
|
||||
"enum": [
|
||||
"Telemetry",
|
||||
"Debug",
|
||||
"Info",
|
||||
"Warning",
|
||||
"Error"
|
||||
],
|
||||
"default": "Warning",
|
||||
"description": "Sets the logging threshold of the daml-ide and multi-ide"
|
||||
},
|
||||
"daml.profile": {
|
||||
"type": "boolean",
|
||||
@ -115,12 +117,7 @@
|
||||
"daml.multiPackageIdeSupport": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "VERY EXPERIMENTAL: Enables the incomplete and experimental multi-ide feature."
|
||||
},
|
||||
"daml.multiPackageIdeVerbose": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "VERY EXPERIMENTAL: Enables verbose logging from the experimental multi-ide feature."
|
||||
"description": "EXPERIMENTAL: Enables the incomplete and experimental multi-ide feature."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
LanguageClientOptions,
|
||||
RequestType,
|
||||
NotificationType,
|
||||
Executable,
|
||||
ExecuteCommandRequest,
|
||||
} from "vscode-languageclient/node";
|
||||
import {
|
||||
@ -39,85 +40,102 @@ type WebviewFiles = {
|
||||
};
|
||||
|
||||
var damlLanguageClient: LanguageClient;
|
||||
var virtualResourceManager: VirtualResourceManager;
|
||||
var isMultiIde: boolean;
|
||||
|
||||
// Extension activation
|
||||
// Note: You can log debug information by using `console.log()`
|
||||
// and then `Toggle Developer Tools` in VSCode. This will show
|
||||
// output in the Console tab once the extension is activated.
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
// Start the language clients
|
||||
let config = vscode.workspace.getConfiguration("daml");
|
||||
// Get telemetry consent
|
||||
const consent = getTelemetryConsent(config, context);
|
||||
// Add entry for multi-ide readonly directory
|
||||
let filesConfig = vscode.workspace.getConfiguration("files");
|
||||
let multiIdeReadOnlyPattern = "**/.daml/unpacked-dars/**";
|
||||
// Explicit any type as typescript gets angry, its a map from pattern (string) to boolean
|
||||
let readOnlyInclude: any =
|
||||
filesConfig.inspect("readonlyInclude")?.workspaceValue || {};
|
||||
if (!readOnlyInclude[multiIdeReadOnlyPattern])
|
||||
filesConfig.update(
|
||||
"readonlyInclude",
|
||||
{ ...readOnlyInclude, [multiIdeReadOnlyPattern]: true },
|
||||
vscode.ConfigurationTarget.Workspace,
|
||||
);
|
||||
|
||||
// Display release notes on updates
|
||||
showReleaseNotesIfNewVersion(context);
|
||||
|
||||
damlLanguageClient = createLanguageClient(config, await consent);
|
||||
damlLanguageClient.registerProposedFeatures();
|
||||
|
||||
const webviewFiles: WebviewFiles = {
|
||||
src: vscode.Uri.file(path.join(context.extensionPath, "src", "webview.js")),
|
||||
css: vscode.Uri.file(
|
||||
path.join(context.extensionPath, "src", "webview.css"),
|
||||
),
|
||||
};
|
||||
let virtualResourceManager = new VirtualResourceManager(
|
||||
damlLanguageClient,
|
||||
webviewFiles,
|
||||
context,
|
||||
|
||||
async function shutdownLanguageServer() {
|
||||
// Stop the Language server
|
||||
stopKeepAliveWatchdog();
|
||||
await damlLanguageClient.stop();
|
||||
virtualResourceManager.dispose();
|
||||
const index = context.subscriptions.indexOf(virtualResourceManager, 0);
|
||||
if (index > -1) {
|
||||
context.subscriptions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupLanguageServer(
|
||||
config: vscode.WorkspaceConfiguration,
|
||||
consent: boolean | undefined,
|
||||
) {
|
||||
damlLanguageClient = createLanguageClient(config, consent);
|
||||
damlLanguageClient.registerProposedFeatures();
|
||||
|
||||
virtualResourceManager = new VirtualResourceManager(
|
||||
damlLanguageClient,
|
||||
webviewFiles,
|
||||
context,
|
||||
);
|
||||
context.subscriptions.push(virtualResourceManager);
|
||||
|
||||
let _unused = damlLanguageClient.onReady().then(() => {
|
||||
startKeepAliveWatchdog();
|
||||
damlLanguageClient.onNotification(
|
||||
DamlVirtualResourceDidChangeNotification.type,
|
||||
params =>
|
||||
virtualResourceManager.setContent(params.uri, params.contents),
|
||||
);
|
||||
damlLanguageClient.onNotification(
|
||||
DamlVirtualResourceNoteNotification.type,
|
||||
params => virtualResourceManager.setNote(params.uri, params.note),
|
||||
);
|
||||
damlLanguageClient.onNotification(
|
||||
DamlVirtualResourceDidProgressNotification.type,
|
||||
params =>
|
||||
virtualResourceManager.setProgress(
|
||||
params.uri,
|
||||
params.millisecondsPassed,
|
||||
params.startedAt,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
damlLanguageClient.start();
|
||||
}
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration(
|
||||
async (event: vscode.ConfigurationChangeEvent) => {
|
||||
if (event.affectsConfiguration("daml")) {
|
||||
await shutdownLanguageServer();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const config = vscode.workspace.getConfiguration("daml");
|
||||
const consent = await getTelemetryConsent(config, context);
|
||||
setupLanguageServer(config, consent);
|
||||
}
|
||||
},
|
||||
);
|
||||
context.subscriptions.push(virtualResourceManager);
|
||||
|
||||
let _unused = damlLanguageClient.onReady().then(() => {
|
||||
startKeepAliveWatchdog();
|
||||
damlLanguageClient.onNotification(
|
||||
DamlVirtualResourceDidChangeNotification.type,
|
||||
params => virtualResourceManager.setContent(params.uri, params.contents),
|
||||
);
|
||||
damlLanguageClient.onNotification(
|
||||
DamlVirtualResourceNoteNotification.type,
|
||||
params => virtualResourceManager.setNote(params.uri, params.note),
|
||||
);
|
||||
damlLanguageClient.onNotification(
|
||||
DamlVirtualResourceDidProgressNotification.type,
|
||||
params =>
|
||||
virtualResourceManager.setProgress(
|
||||
params.uri,
|
||||
params.millisecondsPassed,
|
||||
params.startedAt,
|
||||
),
|
||||
);
|
||||
vscode.workspace.onDidChangeConfiguration(
|
||||
(event: vscode.ConfigurationChangeEvent) => {
|
||||
if (event.affectsConfiguration("daml.multiPackageIdeSupport")) {
|
||||
const enabled = vscode.workspace
|
||||
.getConfiguration("daml")
|
||||
.get("multiPackageIdeSupport");
|
||||
let msg = "VSCode must be reloaded for this change to take effect.";
|
||||
if (enabled)
|
||||
msg =
|
||||
msg +
|
||||
"\nWARNING - The Multi-IDE support is experimental, has bugs, and will likely change without warning. Use at your own risk.";
|
||||
window
|
||||
.showInformationMessage(msg, { modal: true }, "Reload now")
|
||||
.then((option: string | undefined) => {
|
||||
if (option == "Reload now")
|
||||
vscode.commands.executeCommand("workbench.action.reloadWindow");
|
||||
});
|
||||
} else if (event.affectsConfiguration("daml.multiPackageIdeVerbose")) {
|
||||
let msg = "VSCode must be reloaded for this change to take effect.";
|
||||
window
|
||||
.showInformationMessage(msg, { modal: true }, "Reload now")
|
||||
.then((option: string | undefined) => {
|
||||
if (option == "Reload now")
|
||||
vscode.commands.executeCommand("workbench.action.reloadWindow");
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
damlLanguageClient.start();
|
||||
const config = vscode.workspace.getConfiguration("daml");
|
||||
const consent = await getTelemetryConsent(config, context);
|
||||
setupLanguageServer(config, consent);
|
||||
|
||||
let d1 = vscode.commands.registerCommand("daml.showResource", (title, uri) =>
|
||||
virtualResourceManager.createOrShow(title, uri),
|
||||
@ -260,6 +278,42 @@ function addIfInConfig(
|
||||
return [].concat.apply([], <any>addedArgs);
|
||||
}
|
||||
|
||||
function getLanguageServerArgs(
|
||||
config: vscode.WorkspaceConfiguration,
|
||||
telemetryConsent: boolean | undefined,
|
||||
): string[] {
|
||||
const multiIDESupport = config.get("multiPackageIdeSupport");
|
||||
isMultiIde = !!multiIDESupport;
|
||||
const logLevel = config.get("logLevel");
|
||||
const isDebug = logLevel == "Debug" || logLevel == "Telemetry";
|
||||
|
||||
let args: string[] = [multiIDESupport ? "multi-ide" : "ide", "--"];
|
||||
|
||||
if (telemetryConsent === true) {
|
||||
args.push("--telemetry");
|
||||
} else if (telemetryConsent === false) {
|
||||
args.push("--optOutTelemetry");
|
||||
} else if (telemetryConsent == undefined) {
|
||||
// The user has not made an explicit choice.
|
||||
args.push("--telemetry-ignored");
|
||||
}
|
||||
if (multiIDESupport === true) {
|
||||
args.push("--log-level=" + logLevel);
|
||||
} else {
|
||||
if (isDebug) args.push("--debug");
|
||||
}
|
||||
const extraArgsString = config.get("extraArguments", "").trim();
|
||||
// split on an empty string returns an array with a single empty string
|
||||
const extraArgs = extraArgsString === "" ? [] : extraArgsString.split(" ");
|
||||
args = args.concat(extraArgs);
|
||||
const serverArgs: string[] = addIfInConfig(config, args, [
|
||||
["profile", ["+RTS", "-h", "-RTS"]],
|
||||
["autorunAllTests", ["--studio-auto-run-all-scenarios=yes"]],
|
||||
]);
|
||||
|
||||
return serverArgs;
|
||||
}
|
||||
|
||||
export function createLanguageClient(
|
||||
config: vscode.WorkspaceConfiguration,
|
||||
telemetryConsent: boolean | undefined,
|
||||
@ -270,11 +324,7 @@ export function createLanguageClient(
|
||||
documentSelector: ["daml"],
|
||||
};
|
||||
|
||||
const multiIDESupport = config.get("multiPackageIdeSupport");
|
||||
const multiIDEVerbose = config.get("multiPackageIdeVerbose");
|
||||
|
||||
let command: string;
|
||||
let args: string[] = [multiIDESupport ? "multi-ide" : "ide", "--"];
|
||||
|
||||
try {
|
||||
command = which.sync("daml");
|
||||
@ -290,35 +340,9 @@ export function createLanguageClient(
|
||||
}
|
||||
}
|
||||
|
||||
if (telemetryConsent === true) {
|
||||
args.push("--telemetry");
|
||||
} else if (telemetryConsent === false) {
|
||||
args.push("--optOutTelemetry");
|
||||
} else if (telemetryConsent == undefined) {
|
||||
// The user has not made an explicit choice.
|
||||
args.push("--telemetry-ignored");
|
||||
}
|
||||
if (multiIDEVerbose === true) {
|
||||
args.push("--verbose=yes");
|
||||
}
|
||||
const extraArgsString = config.get("extraArguments", "").trim();
|
||||
// split on an empty string returns an array with a single empty string
|
||||
const extraArgs = extraArgsString === "" ? [] : extraArgsString.split(" ");
|
||||
args = args.concat(extraArgs);
|
||||
const serverArgs: string[] = addIfInConfig(config, args, [
|
||||
["debug", ["--debug"]],
|
||||
["experimental", ["--experimental"]],
|
||||
["profile", ["+RTS", "-h", "-RTS"]],
|
||||
["autorunAllTests", ["--studio-auto-run-all-scenarios=yes"]],
|
||||
]);
|
||||
const serverArgs = getLanguageServerArgs(config, telemetryConsent);
|
||||
|
||||
if (config.get("experimental")) {
|
||||
vscode.window.showWarningMessage(
|
||||
"Daml's Experimental feature flag is enabled, this may cause instability",
|
||||
);
|
||||
}
|
||||
|
||||
return new LanguageClient(
|
||||
const languageClient = new LanguageClient(
|
||||
"daml-language-server",
|
||||
"Daml Language Server",
|
||||
{
|
||||
@ -329,14 +353,16 @@ export function createLanguageClient(
|
||||
clientOptions,
|
||||
true,
|
||||
);
|
||||
return languageClient;
|
||||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() {
|
||||
export async function deactivate() {
|
||||
// unLinkSyntax();
|
||||
// Stop keep-alive watchdog and terminate language server.
|
||||
stopKeepAliveWatchdog();
|
||||
(<any>damlLanguageClient)._childProcess.kill("SIGTERM");
|
||||
if (isMultiIde) await damlLanguageClient.stop();
|
||||
else (<any>damlLanguageClient)._serverProcess.kill("SIGTERM");
|
||||
}
|
||||
|
||||
// Keep alive timer for periodically checking that the server is responding
|
||||
|
@ -207,6 +207,7 @@ da_haskell_library(
|
||||
"safe",
|
||||
"safe-exceptions",
|
||||
"shake",
|
||||
"some",
|
||||
"split",
|
||||
"stm",
|
||||
"tasty",
|
||||
@ -214,6 +215,7 @@ da_haskell_library(
|
||||
"tasty-hunit",
|
||||
"temporary",
|
||||
"text",
|
||||
"time",
|
||||
"transformers",
|
||||
"uniplate",
|
||||
"unordered-containers",
|
||||
|
@ -103,6 +103,7 @@ checkPkgConfig PackageConfigFields {pName, pVersion} =
|
||||
|
||||
data MultiPackageConfigFields = MultiPackageConfigFields
|
||||
{ mpPackagePaths :: [FilePath]
|
||||
, mpDars :: [FilePath]
|
||||
}
|
||||
|
||||
-- | Intermediate of MultiPackageConfigFields that carries links to other config files, before being flattened into a single MultiPackageConfigFields
|
||||
@ -114,7 +115,9 @@ data MultiPackageConfigFieldsIntermediate = MultiPackageConfigFieldsIntermediate
|
||||
-- | Parse the multi-package.yaml file for auto rebuilds/IDE intelligence in multi-package projects
|
||||
parseMultiPackageConfig :: MultiPackageConfig -> Either ConfigError MultiPackageConfigFieldsIntermediate
|
||||
parseMultiPackageConfig multiPackage = do
|
||||
mpiConfigFields <- MultiPackageConfigFields . fromMaybe [] <$> queryMultiPackageConfig ["packages"] multiPackage
|
||||
mpPackagePaths <- fromMaybe [] <$> queryMultiPackageConfig ["packages"] multiPackage
|
||||
mpDars <- fromMaybe [] <$> queryMultiPackageConfig ["dars"] multiPackage
|
||||
let mpiConfigFields = MultiPackageConfigFields {..}
|
||||
mpiOtherConfigFiles <- fromMaybe [] <$> queryMultiPackageConfig ["projects"] multiPackage
|
||||
Right MultiPackageConfigFieldsIntermediate {..}
|
||||
|
||||
@ -195,10 +198,10 @@ findMultiPackageConfig projectPath = do
|
||||
in pure $ if path == newPath then Right Nothing else Left newPath
|
||||
|
||||
canonicalizeMultiPackageConfigIntermediate :: ProjectPath -> MultiPackageConfigFieldsIntermediate -> IO MultiPackageConfigFieldsIntermediate
|
||||
canonicalizeMultiPackageConfigIntermediate projectPath (MultiPackageConfigFieldsIntermediate (MultiPackageConfigFields packagePaths) multiPackagePaths) =
|
||||
canonicalizeMultiPackageConfigIntermediate projectPath (MultiPackageConfigFieldsIntermediate (MultiPackageConfigFields packagePaths darPaths) multiPackagePaths) =
|
||||
withCurrentDirectory (unwrapProjectPath projectPath) $ do
|
||||
MultiPackageConfigFieldsIntermediate
|
||||
<$> (MultiPackageConfigFields <$> traverse canonicalizePath packagePaths)
|
||||
<$> (MultiPackageConfigFields <$> traverse canonicalizePath packagePaths <*> traverse canonicalizePath darPaths)
|
||||
<*> traverse canonicalizePath multiPackagePaths
|
||||
|
||||
-- Given some computation to give a result and dependencies, we explore the entire cyclic graph to give the combined
|
||||
@ -225,7 +228,7 @@ fullParseMultiPackageConfig startPath = do
|
||||
canonMultiPackageConfigI <- canonicalizeMultiPackageConfigIntermediate projectPath multiPackageConfigI
|
||||
pure (ProjectPath <$> mpiOtherConfigFiles canonMultiPackageConfigI, mpiConfigFields canonMultiPackageConfigI)
|
||||
|
||||
pure $ MultiPackageConfigFields $ nubOrd $ concatMap mpPackagePaths mpcs
|
||||
pure $ MultiPackageConfigFields (nubOrd $ concatMap mpPackagePaths mpcs) (nubOrd $ concatMap mpDars mpcs)
|
||||
|
||||
-- Gives the filepath where the multipackage was found if its not the same as project path.
|
||||
withMultiPackageConfig :: ProjectPath -> (MultiPackageConfigFields -> IO a) -> IO a
|
||||
|
@ -34,6 +34,7 @@ import DA.Cli.Options (Debug(..),
|
||||
Style(..),
|
||||
Telemetry(..),
|
||||
cliOptDetailLevel,
|
||||
cliOptLogLevel,
|
||||
debugOpt,
|
||||
disabledDlintUsageParser,
|
||||
enabledDlintUsageParser,
|
||||
@ -45,7 +46,6 @@ import DA.Cli.Options (Debug(..),
|
||||
inputDarOpt,
|
||||
inputFileOpt,
|
||||
inputFileOptWithExt,
|
||||
multiIdeVerboseOpt,
|
||||
multiPackageBuildAllOpt,
|
||||
multiPackageCleanAllOpt,
|
||||
multiPackageLocationOpt,
|
||||
@ -228,6 +228,7 @@ import Options.Applicative ((<|>),
|
||||
execParserPure,
|
||||
flag,
|
||||
flag',
|
||||
forwardOptions,
|
||||
fullDesc,
|
||||
handleParseResult,
|
||||
headerDoc,
|
||||
@ -242,6 +243,7 @@ import Options.Applicative ((<|>),
|
||||
prefs,
|
||||
progDesc,
|
||||
renderFailure,
|
||||
strArgument,
|
||||
subparser,
|
||||
switch,
|
||||
value)
|
||||
@ -307,15 +309,17 @@ data CommandName =
|
||||
deriving (Ord, Show, Eq)
|
||||
data Command = Command CommandName (Maybe ProjectOpts) (IO ())
|
||||
|
||||
cmdMultiIde :: Int -> Mod CommandFields Command
|
||||
cmdMultiIde :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
|
||||
cmdMultiIde _numProcessors =
|
||||
command "multi-ide" $ info (helper <*> cmd) $
|
||||
progDesc
|
||||
"Start the Daml Multi-IDE language server on standard input/output."
|
||||
<> fullDesc
|
||||
<> forwardOptions
|
||||
where
|
||||
cmd = fmap (Command MultiIde Nothing) $ runMultiIde
|
||||
<$> multiIdeVerboseOpt
|
||||
<$> cliOptLogLevel
|
||||
<*> many (strArgument mempty)
|
||||
|
||||
cmdIde :: SdkVersion.Class.SdkVersioned => Int -> Mod CommandFields Command
|
||||
cmdIde numProcessors =
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,199 @@
|
||||
-- Copyright (c) 2024 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
-- SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
module DA.Cli.Damlc.Command.MultiIde.DarDependencies (resolveSourceLocation, unpackDar, unpackedDarsLocation) where
|
||||
|
||||
import "zip-archive" Codec.Archive.Zip (Archive (..), Entry(..), toArchive, toEntry, fromArchive, fromEntry, findEntryByPath, deleteEntryFromArchive)
|
||||
import Control.Monad (forM_, void)
|
||||
import DA.Cli.Damlc.Command.MultiIde.Types (MultiIdeState (..), PackageSourceLocation (..), PackageHome (..), DarFile (..), logDebug, logInfo)
|
||||
import DA.Daml.Compiler.Dar (breakAt72Bytes, mkConfFile)
|
||||
import qualified DA.Daml.LF.Ast.Base as LF
|
||||
import qualified DA.Daml.LF.Ast.Version as LF
|
||||
import DA.Daml.LF.Proto3.Archive (DecodingMode (..), decodeArchive)
|
||||
import DA.Daml.LF.Reader (DalfManifest(..), readManifest, readDalfManifest)
|
||||
import DA.Daml.Project.Consts (projectConfigName)
|
||||
import Data.Bifunctor (second)
|
||||
import qualified Data.ByteString.Char8 as BSC
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import qualified Data.ByteString.Lazy.Char8 as BSLC
|
||||
import qualified Data.ByteString as BS
|
||||
import Data.List (delete, intercalate, isPrefixOf)
|
||||
import Data.List.Extra (lastDef, unsnoc)
|
||||
import Data.List.Split (splitOn)
|
||||
import qualified Data.Map as Map
|
||||
import Data.Map (Map)
|
||||
import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import qualified Data.NameMap as NM
|
||||
import qualified Data.Text as T
|
||||
import Data.Tuple.Extra (fst3, thd3)
|
||||
import System.Directory (createDirectoryIfMissing, doesFileExist, removePathForcibly)
|
||||
import System.FilePath.Posix
|
||||
|
||||
import qualified Module as Ghc
|
||||
|
||||
-- Given a dar, attempts to recreate the package structure for the IDE, with all files set to read-only.
|
||||
-- Note, this function deletes the previous folder for the same unit-id, ensure subIDE is not running in this directory
|
||||
-- before calling this function
|
||||
unpackDar :: MultiIdeState -> DarFile -> IO ()
|
||||
unpackDar miState darFile = do
|
||||
let darPath = unDarFile darFile
|
||||
logInfo miState $ "Unpacking dar: " <> darPath
|
||||
archiveWithSource <- toArchive <$> BSL.readFile darPath
|
||||
manifest <- either fail pure $ readDalfManifest archiveWithSource
|
||||
rawManifest <- either fail pure $ readManifest archiveWithSource
|
||||
let (archive, damlFiles) = extractDarSourceFiles archiveWithSource
|
||||
|
||||
mainDalf <- maybe (fail "Couldn't find main dalf in dar") pure $ findEntryByPath (mainDalfPath manifest) archive
|
||||
|
||||
let (mainPkgName, mainPkgVersion, mainPackageId) = extractPackageMetadataFromEntry mainDalf
|
||||
darUnpackLocation = unPackageHome $ unpackedDarPath miState mainPkgName mainPkgVersion
|
||||
|
||||
-- Clear the unpack location
|
||||
removePathForcibly darUnpackLocation
|
||||
|
||||
-- Write packageId file
|
||||
createDirectoryIfMissing True (darUnpackLocation </> ".daml")
|
||||
writeFile (darUnpackLocation </> ".daml" </> mainPackageId) ""
|
||||
|
||||
void $ flip Map.traverseWithKey damlFiles $ \path content -> do
|
||||
let fullPath = darUnpackLocation </> "daml" </> path
|
||||
createDirectoryIfMissing True (takeDirectory fullPath)
|
||||
BSL.writeFile fullPath content
|
||||
|
||||
let mainDalfContent = BSL.toStrict $ fromEntry mainDalf
|
||||
ignoredPrefixes = ["daml-stdlib", "daml-prim", "daml-script", "daml3-script", mainPkgName <> "-" <> mainPkgVersion]
|
||||
-- Filter dalfs first such that none start with `daml-stdlib` or `daml-prim`, `daml-script` or `daml3-script`
|
||||
-- then that the package id of the dalf isn't in the LF for the main package
|
||||
dalfsToExpand =
|
||||
flip filter (zEntries archive) $ \entry ->
|
||||
takeExtension (eRelativePath entry) == ".dalf"
|
||||
&& not (any (\prefix -> prefix `isPrefixOf` takeBaseName (eRelativePath entry)) ignoredPrefixes)
|
||||
&& BS.isInfixOf (BSC.pack $ thd3 $ extractPackageMetadataFromEntry entry) mainDalfContent
|
||||
-- Rebuild dalfs into full dars under dars directory
|
||||
darDepArchives =
|
||||
fmap (\entry ->
|
||||
( darUnpackLocation </> "dars" </> takeBaseName (eRelativePath entry) <.> "dar"
|
||||
, rebuildDarFromDalfEntry archive rawManifest (dalfPaths manifest) (eRelativePath mainDalf) entry
|
||||
)
|
||||
) dalfsToExpand
|
||||
|
||||
-- Write dar files
|
||||
forM_ darDepArchives $ \(path, archive) -> do
|
||||
createDirectoryIfMissing True (takeDirectory path)
|
||||
BSL.writeFile path $ fromArchive archive
|
||||
|
||||
(_, mainPkg) <- either (fail . show) pure $ decodeArchive DecodeAsMain mainDalfContent
|
||||
|
||||
let isSdkPackage pkgName entry =
|
||||
takeExtension (eRelativePath entry) == ".dalf" && pkgName == fst3 (extractPackageMetadataFromEntry entry)
|
||||
includesSdkPackage pkgName = any (isSdkPackage pkgName) $ zEntries archive
|
||||
sdkPackages = ["daml-script", "daml3-script", "daml-trigger"]
|
||||
deps = ["daml-prim", "daml-stdlib"] <> filter includesSdkPackage sdkPackages
|
||||
damlYamlContent = unlines $
|
||||
[ "sdk-version: " <> sdkVersion manifest
|
||||
, "name: " <> T.unpack (LF.unPackageName $ LF.packageName $ LF.packageMetadata mainPkg)
|
||||
, "version: " <> T.unpack (LF.unPackageVersion $ LF.packageVersion $ LF.packageMetadata mainPkg)
|
||||
, "source: daml"
|
||||
, "build-options:"
|
||||
, " - --target=" <> LF.renderVersion (LF.packageLfVersion mainPkg)
|
||||
, "dependencies:"
|
||||
]
|
||||
<> fmap (" - " <>) deps
|
||||
<> ["data-dependencies: "]
|
||||
<> fmap (\(path, _) -> " - " <> makeRelative darUnpackLocation path) darDepArchives
|
||||
|
||||
writeFile (darUnpackLocation </> projectConfigName) damlYamlContent
|
||||
|
||||
extractPackageMetadataFromEntry :: Entry -> (String, String, String)
|
||||
extractPackageMetadataFromEntry = extractPackageMetadataFromDalfPath . eRelativePath
|
||||
|
||||
-- Gives back name, version, package hash
|
||||
-- TODO: Ensure this information is always here and of this form
|
||||
extractPackageMetadataFromDalfPath :: FilePath -> (String, String, String)
|
||||
extractPackageMetadataFromDalfPath path =
|
||||
case unsnoc $ splitOn "-" $ takeBaseName path of
|
||||
Just ([name], hash) -> (name, "", hash)
|
||||
Just (sections, hash) -> (intercalate "-" $ init sections, lastDef "" sections, hash)
|
||||
_ -> ("", "", "")
|
||||
|
||||
unpackedDarsLocation :: MultiIdeState -> FilePath
|
||||
unpackedDarsLocation miState = multiPackageHome miState </> ".daml" </> "unpacked-dars"
|
||||
|
||||
unpackedDarPath :: MultiIdeState -> String -> String -> PackageHome
|
||||
unpackedDarPath miState pkgName pkgVersion = PackageHome $ unpackedDarsLocation miState </> pkgName <> "-" <> pkgVersion
|
||||
|
||||
-- Pull out every daml file into a mapping from path to content
|
||||
-- Return an archive without these files or any hi/hie files
|
||||
extractDarSourceFiles :: Archive -> (Archive, Map FilePath BSL.ByteString)
|
||||
extractDarSourceFiles archive = foldr handleEntry (archive, Map.empty) $ zEntries archive
|
||||
where
|
||||
handleEntry :: Entry -> (Archive, Map FilePath BSL.ByteString) -> (Archive, Map FilePath BSL.ByteString)
|
||||
handleEntry entry (archive', damlFiles) =
|
||||
case takeExtension $ eRelativePath entry of
|
||||
".daml" -> (deleteEntryFromArchive (eRelativePath entry) archive', Map.insert (joinPath $ tail $ splitPath $ eRelativePath entry) (fromEntry entry) damlFiles)
|
||||
".hi" -> (deleteEntryFromArchive (eRelativePath entry) archive', damlFiles)
|
||||
".hie" -> (deleteEntryFromArchive (eRelativePath entry) archive', damlFiles)
|
||||
_ -> (archive', damlFiles)
|
||||
|
||||
-- Recreate the conf file from a dalf
|
||||
readDalfConf :: Entry -> (FilePath, BSL.ByteString)
|
||||
readDalfConf entry =
|
||||
let (pkgId :: LF.PackageId, pkg :: LF.Package) = either (error . show) id $ decodeArchive DecodeAsMain $ BSL.toStrict $ fromEntry entry
|
||||
moduleNames :: [Ghc.ModuleName]
|
||||
moduleNames = Ghc.mkModuleName . T.unpack . T.intercalate "." . LF.unModuleName <$> NM.names (LF.packageModules pkg)
|
||||
pkgName :: LF.PackageName
|
||||
pkgName = LF.packageName $ LF.packageMetadata pkg
|
||||
pkgVersion :: LF.PackageVersion
|
||||
pkgVersion = LF.packageVersion $ LF.packageMetadata pkg
|
||||
-- TODO[SW]: the `depends` list is empty right now, as we don't have the full dar dependency tree.
|
||||
in second BSL.fromStrict $ mkConfFile pkgName (Just pkgVersion) [] Nothing moduleNames pkgId
|
||||
|
||||
-- Copies all dalf files over, changing their directory to match the new main package
|
||||
-- Updates the Name, Main-Dalf and Dalfs fields in the manifest to reflect the new main package/dalf locations
|
||||
-- Updates the <package>/data/<package>.conf file to reflect the new package (note that the "depends" field is a little tricky)
|
||||
rebuildDarFromDalfEntry :: Archive -> [(BS.ByteString, BS.ByteString)] -> [FilePath] -> FilePath -> Entry -> Archive
|
||||
rebuildDarFromDalfEntry archive rawManifest dalfPaths topDalfPath mainEntry = archive {zEntries = mapMaybe mapEntry $ zEntries archive}
|
||||
where
|
||||
mapEntry :: Entry -> Maybe Entry
|
||||
mapEntry entry =
|
||||
case takeExtension $ eRelativePath entry of
|
||||
-- Need to remove the top level dar
|
||||
".dalf" | eRelativePath entry == topDalfPath -> Nothing
|
||||
".dalf" -> Just $ entry {eRelativePath = updatePathToMainEntry $ eRelativePath entry}
|
||||
".MF" -> Just $ toEntry (eRelativePath entry) (eLastModified entry) $ serialiseRawManifest $ overwriteRawManifestFields rawManifest
|
||||
[ ("Name", BSC.pack mainEntryId)
|
||||
, ("Main-Dalf", BSC.pack $ updatePathToMainEntry $ eRelativePath mainEntry)
|
||||
, ("Dalfs", BS.intercalate ", " $ BSC.pack . updatePathToMainEntry <$> dalfPathsWithoutTop)
|
||||
]
|
||||
".conf" ->
|
||||
let (confFileName, confContent) = readDalfConf mainEntry
|
||||
in Just $ toEntry
|
||||
(mainEntryName </> "data" </> confFileName)
|
||||
(eLastModified entry)
|
||||
confContent
|
||||
_ -> Just entry
|
||||
dalfPathsWithoutTop = delete topDalfPath dalfPaths
|
||||
mainEntryName = takeBaseName $ eRelativePath mainEntry
|
||||
mainEntryId = intercalate "-" $ init $ splitOn "-" mainEntryName
|
||||
updatePathToMainEntry = joinPath . (mainEntryName :) . tail . splitPath
|
||||
serialiseRawManifest :: [(BS.ByteString, BS.ByteString)] -> BSL.ByteString
|
||||
serialiseRawManifest = BSLC.unlines . map (\(k, v) -> breakAt72Bytes $ BSL.fromStrict $ k <> ": " <> v)
|
||||
overwriteRawManifestFields :: [(BS.ByteString, BS.ByteString)] -> [(BS.ByteString, BS.ByteString)] -> [(BS.ByteString, BS.ByteString)]
|
||||
overwriteRawManifestFields original overwrites' = fmap (\(k, v) -> (k, fromMaybe v $ Map.lookup k overwrites)) original
|
||||
where
|
||||
overwrites = Map.fromList overwrites'
|
||||
|
||||
-- Resolves the source location of a package location to a path, alongside an optional path to a dar to unpack first
|
||||
resolveSourceLocation :: MultiIdeState -> PackageSourceLocation -> IO (PackageHome, Maybe DarFile)
|
||||
resolveSourceLocation _ (PackageOnDisk path) = pure (path, Nothing)
|
||||
resolveSourceLocation miState (PackageInDar darPath) = do
|
||||
logDebug miState "Looking for unpacked dar"
|
||||
archive <- toArchive <$> BSL.readFile (unDarFile darPath)
|
||||
manifest <- either fail pure $ readDalfManifest archive
|
||||
let (pkgName, pkgVersion, pkgId) = extractPackageMetadataFromDalfPath $ mainDalfPath manifest
|
||||
pkgPath = unpackedDarPath miState pkgName pkgVersion
|
||||
pkgIdTagPath = unPackageHome pkgPath </> ".daml" </> pkgId
|
||||
|
||||
pkgExists <- doesFileExist pkgIdTagPath
|
||||
|
||||
pure (pkgPath, if pkgExists then Nothing else Just darPath)
|
@ -51,7 +51,7 @@ pullMonadThroughTuple (a, mb) = (a,) <$> mb
|
||||
-- Takes a natural transformation of responses and lifts it to forward the first error
|
||||
assumeSuccessCombiner
|
||||
:: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request)
|
||||
. ([(FilePath, LSP.ResponseResult m)] -> LSP.ResponseResult m)
|
||||
. ([(PackageHome, LSP.ResponseResult m)] -> LSP.ResponseResult m)
|
||||
-> ResponseCombiner m
|
||||
assumeSuccessCombiner f res = f <$> mapM pullMonadThroughTuple res
|
||||
|
||||
@ -78,36 +78,37 @@ uriFilePathPrism = prism' LSP.filePathToUri LSP.uriToFilePath
|
||||
|
||||
getMessageForwardingBehaviour
|
||||
:: forall t (m :: LSP.Method 'LSP.FromClient t)
|
||||
. LSP.SMethod m
|
||||
. MultiIdeState
|
||||
-> LSP.SMethod m
|
||||
-> LSP.Message m
|
||||
-> Forwarding m
|
||||
getMessageForwardingBehaviour meth params =
|
||||
getMessageForwardingBehaviour miState meth params =
|
||||
case meth of
|
||||
LSP.SInitialize -> handleElsewhere "Initialize"
|
||||
LSP.SInitialized -> ignore
|
||||
-- send to all then const reply
|
||||
LSP.SShutdown -> ForwardRequest params $ AllRequest (assumeSuccessCombiner @m $ const LSP.Empty)
|
||||
LSP.SExit -> ForwardNotification params AllNotification
|
||||
LSP.SExit -> handleElsewhere "Exit"
|
||||
LSP.SWorkspaceDidChangeWorkspaceFolders -> ForwardNotification params AllNotification
|
||||
LSP.SWorkspaceDidChangeConfiguration -> ForwardNotification params AllNotification
|
||||
LSP.SWorkspaceDidChangeWatchedFiles -> ForwardNotification params AllNotification
|
||||
LSP.STextDocumentDidOpen -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentDidChange -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentWillSave -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentWillSaveWaitUntil -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentDidSave -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentDidClose -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentCompletion -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentHover -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentSignatureHelp -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentDeclaration -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentDidOpen -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentDidChange -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentWillSave -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentWillSaveWaitUntil -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentDidSave -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentDidClose -> ForwardNotification params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentCompletion -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentHover -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentSignatureHelp -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentDeclaration -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentDefinition -> handleElsewhere "TextDocumentDefinition"
|
||||
LSP.STextDocumentDocumentSymbol -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentCodeAction -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentCodeLens -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentDocumentLink -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentColorPresentation -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentOnTypeFormatting -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument params
|
||||
LSP.STextDocumentDocumentSymbol -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentCodeAction -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentCodeLens -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentDocumentLink -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentColorPresentation -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
LSP.STextDocumentOnTypeFormatting -> ForwardRequest params $ forwardingBehaviourFromParamsWithTextDocument miState params
|
||||
|
||||
LSP.SCustomMethod "daml/keepAlive" ->
|
||||
case params of
|
||||
@ -169,20 +170,25 @@ getMessageForwardingBehaviour meth params =
|
||||
LSP.STextDocumentSemanticTokensRange -> unsupported "TextDocumentSemanticTokensRange"
|
||||
LSP.SWorkspaceSemanticTokensRefresh -> unsupported "WorkspaceSemanticTokensRefresh"
|
||||
|
||||
filePathFromParamsWithTextDocument :: (LSP.HasParams p a, LSP.HasTextDocument a t, LSP.HasUri t LSP.Uri) => p -> FilePath
|
||||
filePathFromParamsWithTextDocument params =
|
||||
filePathFromParamsWithTextDocument :: (LSP.HasParams p a, LSP.HasTextDocument a t, LSP.HasUri t LSP.Uri) => MultiIdeState -> p -> FilePath
|
||||
filePathFromParamsWithTextDocument miState params =
|
||||
let uri = params ^. LSP.params . LSP.textDocument . LSP.uri
|
||||
in fromMaybe (error $ "Failed to extract path: " <> show uri) $ filePathFromURI uri
|
||||
in fromMaybe (error $ "Failed to extract path: " <> show uri) $ filePathFromURI miState uri
|
||||
|
||||
forwardingBehaviourFromParamsWithTextDocument :: (LSP.HasParams p a, LSP.HasTextDocument a t, LSP.HasUri t LSP.Uri) => p -> ForwardingBehaviour m
|
||||
forwardingBehaviourFromParamsWithTextDocument params = Single $ filePathFromParamsWithTextDocument params
|
||||
forwardingBehaviourFromParamsWithTextDocument :: (LSP.HasParams p a, LSP.HasTextDocument a t, LSP.HasUri t LSP.Uri) => MultiIdeState -> p -> ForwardingBehaviour m
|
||||
forwardingBehaviourFromParamsWithTextDocument miState params = Single $ filePathFromParamsWithTextDocument miState params
|
||||
|
||||
-- Attempts to convert the URI directly to a filepath
|
||||
-- If the URI is a virtual resource, we instead parse it as such and extract the file from that
|
||||
filePathFromURI :: LSP.Uri -> Maybe FilePath
|
||||
filePathFromURI uri =
|
||||
filePathFromURI :: MultiIdeState -> LSP.Uri -> Maybe FilePath
|
||||
filePathFromURI miState uri =
|
||||
LSP.uriToFilePath uri
|
||||
<|> do
|
||||
parsedUri <- URI.parseURI $ T.unpack $ LSP.getUri uri
|
||||
vr <- uriToVirtualResource parsedUri
|
||||
pure $ LSP.fromNormalizedFilePath $ vrScenarioFile vr
|
||||
case URI.uriScheme parsedUri of
|
||||
"daml:" -> do
|
||||
vr <- uriToVirtualResource parsedUri
|
||||
pure $ LSP.fromNormalizedFilePath $ vrScenarioFile vr
|
||||
"untitled:" ->
|
||||
pure $ unPackageHome $ defaultPackagePath miState
|
||||
_ -> Nothing
|
||||
|
@ -10,6 +10,8 @@
|
||||
{-# LANGUAGE GADTs #-}
|
||||
|
||||
module DA.Cli.Damlc.Command.MultiIde.Parsing (
|
||||
getUnrespondedRequestsToResend,
|
||||
getUnrespondedRequestsFallbackResponses,
|
||||
onChunks,
|
||||
parseClientMessageWithTracker,
|
||||
parseServerMessageWithTracker,
|
||||
@ -17,7 +19,9 @@ module DA.Cli.Damlc.Command.MultiIde.Parsing (
|
||||
putReqMethodAll,
|
||||
putReqMethodSingleFromClient,
|
||||
putReqMethodSingleFromServer,
|
||||
putServerReq,
|
||||
putReqMethodSingleFromServerCoordinator,
|
||||
putFromServerMessage,
|
||||
putSingleFromClientMessage,
|
||||
) where
|
||||
|
||||
import Control.Concurrent.STM.TVar
|
||||
@ -31,12 +35,18 @@ import qualified Data.ByteString.Lazy as BSL
|
||||
import qualified Data.ByteString.Lazy.Char8 as BSLC
|
||||
import Data.Foldable (forM_)
|
||||
import DA.Cli.Damlc.Command.MultiIde.Types
|
||||
import DA.Cli.Damlc.Command.MultiIde.Util
|
||||
import Data.Bifunctor (second)
|
||||
import Data.Functor.Product
|
||||
import qualified Data.IxMap as IM
|
||||
import Data.List (delete)
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Some.Newtype (Some, mkSome, withSome)
|
||||
import qualified Language.LSP.Types as LSP
|
||||
import qualified Language.LSP.Types.Lens as LSP
|
||||
import System.IO.Extra
|
||||
import Unsafe.Coerce (unsafeCoerce)
|
||||
|
||||
-- Missing from Data.Attoparsec.ByteString.Lazy, copied from Data.Attoparsec.ByteString.Char8
|
||||
decimal :: Attoparsec.Parser Int
|
||||
@ -51,11 +61,13 @@ contentChunkParser = do
|
||||
Attoparsec.take len
|
||||
|
||||
-- Runs a handler on chunks as they come through the handle
|
||||
-- Returns an error string on failure
|
||||
onChunks :: Handle -> (B.ByteString -> IO ()) -> IO ()
|
||||
onChunks handle act =
|
||||
let handleResult bytes =
|
||||
case Attoparsec.parse contentChunkParser bytes of
|
||||
Attoparsec.Done leftovers result -> act result >> handleResult leftovers
|
||||
Attoparsec.Fail _ _ "not enough input" -> pure ()
|
||||
Attoparsec.Fail _ _ err -> error $ "Chunk parse failed: " <> err
|
||||
in BSL.hGetContents handle >>= handleResult
|
||||
|
||||
@ -63,28 +75,52 @@ putChunk :: Handle -> BSL.ByteString -> IO ()
|
||||
putChunk handle payload = do
|
||||
let fullMessage = "Content-Length: " <> BSLC.pack (show (BSL.length payload)) <> "\r\n\r\n" <> payload
|
||||
BSL.hPut handle fullMessage
|
||||
hFlush handle
|
||||
hTryFlush handle
|
||||
|
||||
putReqMethodSingleFromServer
|
||||
:: forall (m :: LSP.Method 'LSP.FromServer 'LSP.Request)
|
||||
. MethodTrackerVar 'LSP.FromServer -> FilePath -> LSP.LspId m -> LSP.SMethod m -> IO ()
|
||||
putReqMethodSingleFromServer tracker home id method = putReqMethod tracker id $ TrackedSingleMethodFromServer method home
|
||||
. MethodTrackerVar 'LSP.FromServer -> PackageHome -> LSP.LspId m -> LSP.SMethod m -> IO ()
|
||||
putReqMethodSingleFromServer tracker home id method = putReqMethod tracker id $ TrackedSingleMethodFromServer method $ Just home
|
||||
|
||||
putReqMethodSingleFromServerCoordinator
|
||||
:: forall (m :: LSP.Method 'LSP.FromServer 'LSP.Request)
|
||||
. MethodTrackerVar 'LSP.FromServer -> LSP.LspId m -> LSP.SMethod m -> IO ()
|
||||
putReqMethodSingleFromServerCoordinator tracker id method = putReqMethod tracker id $ TrackedSingleMethodFromServer method Nothing
|
||||
|
||||
-- Takes a message from server and stores it if its a request, so that later messages from the client can deduce response context
|
||||
putFromServerMessage :: MultiIdeState -> PackageHome -> LSP.FromServerMessage -> IO ()
|
||||
putFromServerMessage miState home (LSP.FromServerMess method mess) =
|
||||
case (LSP.splitServerMethod method, mess) of
|
||||
(LSP.IsServerReq, _) -> putReqMethodSingleFromServer (fromServerMethodTrackerVar miState) home (mess ^. LSP.id) method
|
||||
(LSP.IsServerEither, LSP.ReqMess mess) -> putReqMethodSingleFromServer (fromServerMethodTrackerVar miState) home (mess ^. LSP.id) method
|
||||
_ -> pure ()
|
||||
putFromServerMessage _ _ _ = pure ()
|
||||
|
||||
putReqMethodSingleFromClient
|
||||
:: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request)
|
||||
. MethodTrackerVar 'LSP.FromClient -> LSP.LspId m -> LSP.SMethod m -> IO ()
|
||||
putReqMethodSingleFromClient tracker id method = putReqMethod tracker id $ TrackedSingleMethodFromClient method
|
||||
. MethodTrackerVar 'LSP.FromClient -> LSP.LspId m -> LSP.SMethod m -> LSP.FromClientMessage -> PackageHome -> IO ()
|
||||
putReqMethodSingleFromClient tracker id method message home = putReqMethod tracker id $ TrackedSingleMethodFromClient method message home
|
||||
|
||||
-- Convenience wrapper around putReqMethodSingleFromClient
|
||||
putSingleFromClientMessage :: MultiIdeState -> PackageHome -> LSP.FromClientMessage -> IO ()
|
||||
putSingleFromClientMessage miState home msg@(LSP.FromClientMess method mess) =
|
||||
case (LSP.splitClientMethod method, mess) of
|
||||
(LSP.IsClientReq, _) -> putReqMethodSingleFromClient (fromClientMethodTrackerVar miState) (mess ^. LSP.id) method msg home
|
||||
(LSP.IsClientEither, LSP.ReqMess mess) -> putReqMethodSingleFromClient (fromClientMethodTrackerVar miState) (mess ^. LSP.id) method msg home
|
||||
_ -> pure ()
|
||||
putSingleFromClientMessage _ _ _ = pure ()
|
||||
|
||||
putReqMethodAll
|
||||
:: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request)
|
||||
. MethodTrackerVar 'LSP.FromClient
|
||||
-> LSP.LspId m
|
||||
-> LSP.SMethod m
|
||||
-> [FilePath]
|
||||
-> LSP.FromClientMessage
|
||||
-> [PackageHome]
|
||||
-> ResponseCombiner m
|
||||
-> IO ()
|
||||
putReqMethodAll tracker id method ides combine =
|
||||
putReqMethod tracker id $ TrackedAllMethod method id combine ides []
|
||||
putReqMethodAll tracker id method msg ides combine =
|
||||
putReqMethod tracker id $ TrackedAllMethod method id msg combine ides []
|
||||
|
||||
putReqMethod
|
||||
:: forall (f :: LSP.From) (m :: LSP.Method f 'LSP.Request)
|
||||
@ -121,22 +157,23 @@ wrapParseMessageLookup (mayTM, newIM) =
|
||||
-- Parses a message from the server providing context about previous requests from client
|
||||
-- allowing the server parser to reconstruct typed responses to said requests
|
||||
-- Handles TrackedAllMethod by returning Nothing for messages that do not have enough replies yet.
|
||||
parseServerMessageWithTracker :: MethodTrackerVar 'LSP.FromClient -> FilePath -> Aeson.Value -> IO (Either String (Maybe LSP.FromServerMessage))
|
||||
parseServerMessageWithTracker tracker selfIde val = pickReqMethodTo tracker $ \extract ->
|
||||
parseServerMessageWithTracker :: MethodTrackerVar 'LSP.FromClient -> PackageHome -> Aeson.Value -> IO (Either String (Maybe LSP.FromServerMessage))
|
||||
parseServerMessageWithTracker tracker home val = pickReqMethodTo tracker $ \extract ->
|
||||
case Aeson.parseEither (LSP.parseServerMessage (wrapParseMessageLookup . extract)) val of
|
||||
Right (LSP.FromServerMess meth mess) -> (Right (Just $ LSP.FromServerMess meth mess), Nothing)
|
||||
Right (LSP.FromServerRsp (Pair (TrackedSingleMethodFromClient method) (Const newIxMap)) rsp) -> (Right (Just (LSP.FromServerRsp method rsp)), Just newIxMap)
|
||||
Right (LSP.FromServerRsp (Pair (TrackedSingleMethodFromClient method _ _) (Const newIxMap)) rsp) -> (Right (Just (LSP.FromServerRsp method rsp)), Just newIxMap)
|
||||
-- Multi reply logic, for requests that are sent to all IDEs with responses unified. Required for some queries
|
||||
Right (LSP.FromServerRsp (Pair tm@TrackedAllMethod {} (Const newIxMap)) rsp) -> do
|
||||
-- Haskell gets a little confused when updating existential records, so we need to build a new one
|
||||
let tm' = TrackedAllMethod
|
||||
{ tamMethod = tamMethod tm
|
||||
, tamLspId = tamLspId tm
|
||||
, tamClientMessage = tamClientMessage tm
|
||||
, tamCombiner = tamCombiner tm
|
||||
, tamResponses = (selfIde, LSP._result rsp) : tamResponses tm
|
||||
, tamRemainingResponseIDERoots = delete selfIde $ tamRemainingResponseIDERoots tm
|
||||
, tamResponses = (home, LSP._result rsp) : tamResponses tm
|
||||
, tamRemainingResponsePackageHomes = delete home $ tamRemainingResponsePackageHomes tm
|
||||
}
|
||||
if null $ tamRemainingResponseIDERoots tm'
|
||||
if null $ tamRemainingResponsePackageHomes tm'
|
||||
then let msg = LSP.FromServerRsp (tamMethod tm) $ rsp {LSP._result = tamCombiner tm' (tamResponses tm')}
|
||||
in (Right $ Just msg, Just newIxMap)
|
||||
else let insertedIxMap = fromMaybe newIxMap $ IM.insertIxMap (tamLspId tm) tm' newIxMap
|
||||
@ -149,26 +186,92 @@ parseServerMessageWithTracker tracker selfIde val = pickReqMethodTo tracker $ \e
|
||||
parseClientMessageWithTracker
|
||||
:: MethodTrackerVar 'LSP.FromServer
|
||||
-> Aeson.Value
|
||||
-> IO (Either String (LSP.FromClientMessage' (Product LSP.SMethod (Const FilePath))))
|
||||
-> IO (Either String (LSP.FromClientMessage' SMethodWithSender))
|
||||
parseClientMessageWithTracker tracker val = pickReqMethodTo tracker $ \extract ->
|
||||
case Aeson.parseEither (LSP.parseClientMessage (wrapParseMessageLookup . extract)) val of
|
||||
Right (LSP.FromClientMess meth mess) -> (Right (LSP.FromClientMess meth mess), Nothing)
|
||||
Right (LSP.FromClientRsp (Pair (TrackedSingleMethodFromServer method home) (Const newIxMap)) rsp) ->
|
||||
(Right (LSP.FromClientRsp (Pair method (Const home)) rsp), Just newIxMap)
|
||||
Right (LSP.FromClientRsp (Pair (TrackedSingleMethodFromServer method mHome) (Const newIxMap)) rsp) ->
|
||||
(Right (LSP.FromClientRsp (SMethodWithSender method mHome) rsp), Just newIxMap)
|
||||
Left msg -> (Left msg, Nothing)
|
||||
|
||||
-- Takes a message from server and stores it if its a request, so that later messages from the client can deduce response context
|
||||
putServerReq :: MethodTrackerVar 'LSP.FromServer -> FilePath -> LSP.FromServerMessage -> IO ()
|
||||
putServerReq tracker home msg =
|
||||
case msg of
|
||||
LSP.FromServerMess meth mess ->
|
||||
case LSP.splitServerMethod meth of
|
||||
LSP.IsServerReq ->
|
||||
let LSP.RequestMessage {_id, _method} = mess
|
||||
in putReqMethodSingleFromServer tracker home _id _method
|
||||
LSP.IsServerEither ->
|
||||
case mess of
|
||||
LSP.ReqMess LSP.RequestMessage {_id, _method} -> putReqMethodSingleFromServer tracker home _id _method
|
||||
_ -> pure ()
|
||||
_ -> pure ()
|
||||
_ -> pure ()
|
||||
-- Map.mapAccumWithKey where the replacement value is a Maybe. Accumulator is still updated for `Nothing` values
|
||||
mapMaybeAccumWithKey :: Ord k => (a -> k -> b -> (a, Maybe c)) -> a -> Map.Map k b -> (a, Map.Map k c)
|
||||
mapMaybeAccumWithKey f z = flip Map.foldrWithKey (z, Map.empty) $ \k v (accum, m) ->
|
||||
second (maybe m (\v' -> Map.insert k v' m)) $ f accum k v
|
||||
|
||||
-- Convenience for the longwinded FromClient Some TrackedMethod type
|
||||
type SomeFromClientTrackedMethod = Some @(LSP.Method 'LSP.FromClient 'LSP.Request) TrackedMethod
|
||||
|
||||
-- Sadly some coercions needed here, as IxMap doesn't expose methods to traverse the map safely
|
||||
-- Each usage is explained in comments nearby
|
||||
-- We disable the restricted `unsafeCoerce` warning below
|
||||
{-# ANN adjustClientTrackers ("HLint: ignore Avoid restricted function" :: String) #-}
|
||||
adjustClientTrackers
|
||||
:: forall a
|
||||
. MultiIdeState
|
||||
-> PackageHome
|
||||
-> ( forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request)
|
||||
. LSP.LspId m
|
||||
-> TrackedMethod m
|
||||
-> (Maybe (TrackedMethod m), Maybe a)
|
||||
)
|
||||
-> IO [a]
|
||||
adjustClientTrackers miState home adjuster = atomically $ stateTVar (fromClientMethodTrackerVar miState) $ \tracker ->
|
||||
let doAdjust
|
||||
:: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request)
|
||||
. [a]
|
||||
-> LSP.LspId m
|
||||
-> TrackedMethod m
|
||||
-> ([a], Maybe SomeFromClientTrackedMethod)
|
||||
doAdjust accum lspId tracker = let (mTracker, mV) = adjuster lspId tracker in (maybe accum (:accum) mV, mkSome <$> mTracker)
|
||||
-- In this function, we unpack the SomeLspId to LspId m', then coerce the `m'` to match the `m` of TrackedMethod.
|
||||
-- This invariant is enforced by the interface to IxMaps, and thus is safe.
|
||||
adjust :: [a] -> LSP.SomeLspId -> SomeFromClientTrackedMethod -> ([a], Maybe SomeFromClientTrackedMethod)
|
||||
adjust accum someLspId someTracker = withSome someTracker $ \tracker -> case (tracker, someLspId) of
|
||||
(TrackedSingleMethodFromClient _ _ home', LSP.SomeLspId lspId) | home == home' -> doAdjust accum (unsafeCoerce lspId) tracker
|
||||
(TrackedAllMethod {tamRemainingResponsePackageHomes}, LSP.SomeLspId lspId) | home `elem` tamRemainingResponsePackageHomes -> doAdjust accum (unsafeCoerce lspId) tracker
|
||||
_ -> (accum, Just someTracker)
|
||||
-- We know that the fromClientMethodTrackerVar only contains Trackers for FromClient, but this information is lost in the `Some` inside the IxMap
|
||||
-- We define our `adjust` method safely, by having it know this `FromClient` constraint, then coerce it to bring said constraint into scope.
|
||||
-- (trackerMap :: forall (from :: LSP.From). Map.Map SomeLspId (Some @(Lsp.Method from @LSP.Request) TrackedMethod))
|
||||
-- where `from` is constrained outside the IxMap and as such, enforced weakly (using unsafeCoerce)
|
||||
(accum, trackerMap) = mapMaybeAccumWithKey (unsafeCoerce adjust) [] $ IM.getMap tracker
|
||||
in (accum, IM.IxMap trackerMap)
|
||||
|
||||
-- Checks if a given Shutdown or Initialize lspId is for an IDE that is still closing, and as such, should not be removed
|
||||
isClosingIdeInFlight :: SubIDEData -> LSP.SMethod m -> LSP.LspId m -> Bool
|
||||
isClosingIdeInFlight ideData LSP.SShutdown (LSP.IdString str) = any (\ide -> str == ideMessageIdPrefix ide <> "-shutdown") $ ideDataClosing ideData
|
||||
isClosingIdeInFlight ideData LSP.SInitialize (LSP.IdString str) = any (\ide -> str == ideMessageIdPrefix ide <> "-init") $ ideDataClosing ideData
|
||||
isClosingIdeInFlight _ _ _ = False
|
||||
|
||||
-- Reads all unresponded messages for a given home, gives back the original messages. Ignores and deletes Initialize and Shutdown requests
|
||||
-- but only if no ideClosing ides are using them
|
||||
getUnrespondedRequestsToResend :: MultiIdeState -> SubIDEData -> PackageHome -> IO [LSP.FromClientMessage]
|
||||
getUnrespondedRequestsToResend miState ideData home = adjustClientTrackers miState home $ \lspId tracker -> case tmMethod tracker of
|
||||
-- Keep shutdown/initialize messages that are in use, but don't return them
|
||||
method | isClosingIdeInFlight ideData method lspId -> (Just tracker, Nothing)
|
||||
LSP.SInitialize -> (Nothing, Nothing)
|
||||
LSP.SShutdown -> (Nothing, Nothing)
|
||||
_ -> (Just tracker, Just $ tmClientMessage tracker)
|
||||
|
||||
-- Gets fallback responses for all unresponded requests for a given home.
|
||||
-- For Single IDE requests, we return noIDEReply, and delete the request from the tracker
|
||||
-- For All IDE requests, we delete this home from the aggregate response, and if it is now complete, run the combiner and return the result
|
||||
getUnrespondedRequestsFallbackResponses :: MultiIdeState -> SubIDEData -> PackageHome -> IO [LSP.FromServerMessage]
|
||||
getUnrespondedRequestsFallbackResponses miState ideData home = adjustClientTrackers miState home $ \lspId tracker -> case tracker of
|
||||
-- Keep shutdown/initialize messages that are in use, but don't return them
|
||||
TrackedSingleMethodFromClient method _ _ | isClosingIdeInFlight ideData method lspId -> (Just tracker, Nothing)
|
||||
TrackedSingleMethodFromClient _ msg _ -> (Nothing, noIDEReply msg)
|
||||
tm@TrackedAllMethod {tamRemainingResponsePackageHomes = [home']} | home' == home ->
|
||||
let reply = LSP.FromServerRsp (tamMethod tm) $ LSP.ResponseMessage "2.0" (Just $ tamLspId tm) (tamCombiner tm $ tamResponses tm)
|
||||
in (Nothing, Just reply)
|
||||
TrackedAllMethod {..} ->
|
||||
let tm = TrackedAllMethod
|
||||
{ tamMethod
|
||||
, tamLspId
|
||||
, tamClientMessage
|
||||
, tamCombiner
|
||||
, tamResponses
|
||||
, tamRemainingResponsePackageHomes = delete home tamRemainingResponsePackageHomes
|
||||
}
|
||||
in (Just tm, Nothing)
|
||||
|
@ -206,7 +206,7 @@ stripLspPrefix (LSP.IdString (T.uncons -> Just ('t', rest))) = LSP.IdString $ T.
|
||||
stripLspPrefix t = t
|
||||
|
||||
-- Prefixes applied to builtin and custom requests. Notifications do not have ids, responses do not need this logic.
|
||||
addLspPrefixToServerMessage :: SubIDE -> LSP.FromServerMessage -> LSP.FromServerMessage
|
||||
addLspPrefixToServerMessage :: SubIDEInstance -> LSP.FromServerMessage -> LSP.FromServerMessage
|
||||
addLspPrefixToServerMessage _ res@(LSP.FromServerRsp _ _) = res
|
||||
addLspPrefixToServerMessage ide res@(LSP.FromServerMess method params) =
|
||||
case LSP.splitServerMethod method of
|
||||
|
@ -18,82 +18,181 @@ import Control.Concurrent.STM.TChan
|
||||
import Control.Concurrent.STM.TVar
|
||||
import Control.Concurrent.STM.TMVar
|
||||
import Control.Concurrent.MVar
|
||||
import Control.Monad (void)
|
||||
import Control.Monad.STM
|
||||
import DA.Daml.Project.Types (ProjectPath (..))
|
||||
import qualified Data.ByteString.Lazy as BSL
|
||||
import Data.Function (on)
|
||||
import qualified Data.IxMap as IM
|
||||
import qualified Data.Map as Map
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Set as Set
|
||||
import qualified Data.Text as T
|
||||
import Data.Time.Clock (NominalDiffTime, UTCTime, diffUTCTime)
|
||||
import qualified Language.LSP.Types as LSP
|
||||
import System.IO.Extra
|
||||
import System.Process.Typed (Process)
|
||||
import qualified DA.Service.Logger as Logger
|
||||
import qualified DA.Service.Logger.Impl.IO as Logger
|
||||
|
||||
newtype PackageHome = PackageHome {unPackageHome :: FilePath} deriving (Show, Eq, Ord)
|
||||
|
||||
toProjectPath :: PackageHome -> ProjectPath
|
||||
toProjectPath (PackageHome path) = ProjectPath path
|
||||
|
||||
newtype DarFile = DarFile {unDarFile :: FilePath} deriving (Show, Eq, Ord)
|
||||
newtype DamlFile = DamlFile {unDamlFile :: FilePath} deriving (Show, Eq, Ord)
|
||||
|
||||
newtype UnitId = UnitId {unUnitId :: String} deriving (Show, Eq, Ord)
|
||||
|
||||
data TrackedMethod (m :: LSP.Method from 'LSP.Request) where
|
||||
TrackedSingleMethodFromClient
|
||||
:: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request)
|
||||
. LSP.SMethod m
|
||||
-> LSP.FromClientMessage -- | Store the whole message for re-transmission on subIDE restart
|
||||
-> PackageHome -- | Store the recipient subIDE for this message
|
||||
-> TrackedMethod m
|
||||
TrackedSingleMethodFromServer
|
||||
:: forall (m :: LSP.Method 'LSP.FromServer 'LSP.Request)
|
||||
. LSP.SMethod m
|
||||
-> FilePath -- Also store the IDE that sent the request
|
||||
-> Maybe PackageHome -- | Store the IDE that sent the request (or don't, for requests sent by the coordinator)
|
||||
-> TrackedMethod m
|
||||
TrackedAllMethod :: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request).
|
||||
{ tamMethod :: LSP.SMethod m
|
||||
-- ^ The method of the initial request
|
||||
, tamLspId :: LSP.LspId m
|
||||
, tamClientMessage :: LSP.FromClientMessage
|
||||
-- ^ Store the whole message for re-transmission on subIDE restart
|
||||
, tamCombiner :: ResponseCombiner m
|
||||
-- ^ How to combine the results from each IDE
|
||||
, tamRemainingResponseIDERoots :: [FilePath]
|
||||
, tamRemainingResponsePackageHomes :: [PackageHome]
|
||||
-- ^ The IDES that have not yet replied to this message
|
||||
, tamResponses :: [(FilePath, Either LSP.ResponseError (LSP.ResponseResult m))]
|
||||
, tamResponses :: [(PackageHome, Either LSP.ResponseError (LSP.ResponseResult m))]
|
||||
} -> TrackedMethod m
|
||||
|
||||
tmMethod
|
||||
:: forall (from :: LSP.From) (m :: LSP.Method from 'LSP.Request)
|
||||
. TrackedMethod m
|
||||
-> LSP.SMethod m
|
||||
tmMethod (TrackedSingleMethodFromClient m) = m
|
||||
tmMethod (TrackedSingleMethodFromClient m _ _) = m
|
||||
tmMethod (TrackedSingleMethodFromServer m _) = m
|
||||
tmMethod (TrackedAllMethod {tamMethod}) = tamMethod
|
||||
|
||||
tmClientMessage
|
||||
:: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request)
|
||||
. TrackedMethod m
|
||||
-> LSP.FromClientMessage
|
||||
tmClientMessage (TrackedSingleMethodFromClient _ msg _) = msg
|
||||
tmClientMessage (TrackedAllMethod {tamClientMessage}) = tamClientMessage
|
||||
|
||||
type MethodTracker (from :: LSP.From) = IM.IxMap @(LSP.Method from 'LSP.Request) LSP.LspId TrackedMethod
|
||||
type MethodTrackerVar (from :: LSP.From) = TVar (MethodTracker from)
|
||||
|
||||
data SubIDE = SubIDE
|
||||
data SubIDEInstance = SubIDEInstance
|
||||
{ ideInhandleAsync :: Async ()
|
||||
, ideInHandle :: Handle
|
||||
, ideInHandleChannel :: TChan BSL.ByteString
|
||||
, ideOutHandle :: Handle
|
||||
, ideOutHandleAsync :: Async ()
|
||||
-- ^ For sending messages to that SubIDE
|
||||
, ideProcess :: Process Handle Handle ()
|
||||
, ideHomeDirectory :: FilePath
|
||||
, ideErrHandle :: Handle
|
||||
, ideErrText :: TVar T.Text
|
||||
, ideErrTextAsync :: Async ()
|
||||
, ideProcess :: Process Handle Handle Handle
|
||||
, ideHome :: PackageHome
|
||||
, ideMessageIdPrefix :: T.Text
|
||||
-- ^ Some unique string used to prefix message ids created by the SubIDE, to avoid collisions with other SubIDEs
|
||||
-- We use the stringified process ID
|
||||
, ideActive :: Bool
|
||||
, ideUnitId :: String
|
||||
-- TODO[SW]: This isn't strictly safe since this data exists for a short time after subIDE shutdown, duplicates could be created.
|
||||
, ideUnitId :: UnitId
|
||||
-- ^ Unit ID of the package this SubIDE handles
|
||||
-- Of the form "daml-script-0.0.1"
|
||||
}
|
||||
|
||||
instance Eq SubIDEInstance where
|
||||
-- ideMessageIdPrefix is derived from process id, so this equality is of the process.
|
||||
(==) = (==) `on` ideMessageIdPrefix
|
||||
|
||||
instance Ord SubIDEInstance where
|
||||
-- ideMessageIdPrefix is derived from process id, so this ordering is of the process.
|
||||
compare = compare `on` ideMessageIdPrefix
|
||||
|
||||
-- We store an optional main ide, the currently closing ides (kept only so they can reply to their shutdowns), and open files
|
||||
-- open files must outlive the main subide so we can re-send the TextDocumentDidOpen messages on new ide startup
|
||||
data SubIDEData = SubIDEData
|
||||
{ ideDataHome :: PackageHome
|
||||
, ideDataMain :: Maybe SubIDEInstance
|
||||
, ideDataClosing :: Set.Set SubIDEInstance
|
||||
, ideDataOpenFiles :: Set.Set DamlFile
|
||||
, ideDataFailTimes :: [UTCTime]
|
||||
, ideDataDisabled :: Bool
|
||||
, ideDataLastError :: Maybe String
|
||||
}
|
||||
|
||||
defaultSubIDEData :: PackageHome -> SubIDEData
|
||||
defaultSubIDEData home = SubIDEData home Nothing Set.empty Set.empty [] False Nothing
|
||||
|
||||
lookupSubIde :: PackageHome -> SubIDEs -> SubIDEData
|
||||
lookupSubIde home ides = fromMaybe (defaultSubIDEData home) $ Map.lookup home ides
|
||||
|
||||
ideShouldDisableTimeout :: NominalDiffTime
|
||||
ideShouldDisableTimeout = 5
|
||||
|
||||
ideShouldDisable :: SubIDEData -> Bool
|
||||
ideShouldDisable (ideDataFailTimes -> (t1:t2:_)) = t1 `diffUTCTime` t2 < ideShouldDisableTimeout
|
||||
ideShouldDisable _ = False
|
||||
|
||||
-- SubIDEs placed in a TMVar. The emptyness representents a modification lock.
|
||||
-- The lock unsures the following properties:
|
||||
-- If multiple messages are sent to a new IDE at the same time, the first will create and hold a lock, while the rest wait on that lock (avoid multiple create)
|
||||
-- We never attempt to send messages on a stale IDE. If we ever read SubIDEsVar with the intent to send a message on a SubIDE, we must hold the so a shutdown
|
||||
-- cannot be sent on that IDE until we are done. This ensures that when a shutdown does occur, it is impossible for non-shutdown messages to be added to the
|
||||
-- queue after the shutdown.
|
||||
type SubIDEs = Map.Map FilePath SubIDE
|
||||
type SubIDEs = Map.Map PackageHome SubIDEData
|
||||
type SubIDEsVar = TMVar SubIDEs
|
||||
|
||||
onlyActiveSubIdes :: SubIDEs -> SubIDEs
|
||||
onlyActiveSubIdes = Map.filter ideActive
|
||||
-- Helper functions for holding the subIDEs var
|
||||
withIDEsAtomic :: MultiIdeState -> (SubIDEs -> STM (SubIDEs, a)) -> IO a
|
||||
withIDEsAtomic miState f = atomically $ do
|
||||
ides <- takeTMVar $ subIDEsVar miState
|
||||
(ides', res) <- f ides
|
||||
putTMVar (subIDEsVar miState) ides'
|
||||
pure res
|
||||
|
||||
holdingIDEsAtomic :: MultiIdeState -> (SubIDEs -> STM a) -> IO a
|
||||
holdingIDEsAtomic miState f = withIDEsAtomic miState $ \ides -> (ides,) <$> f ides
|
||||
|
||||
withIDEs :: MultiIdeState -> (SubIDEs -> IO (SubIDEs, a)) -> IO a
|
||||
withIDEs miState f = do
|
||||
ides <- atomically $ takeTMVar $ subIDEsVar miState
|
||||
(ides', res) <- f ides
|
||||
atomically $ putTMVar (subIDEsVar miState) ides'
|
||||
pure res
|
||||
|
||||
holdingIDEs :: MultiIdeState -> (SubIDEs -> IO a) -> IO a
|
||||
holdingIDEs miState f = withIDEs miState $ \ides -> (ides,) <$> f ides
|
||||
|
||||
withIDEs_ :: MultiIdeState -> (SubIDEs -> IO SubIDEs) -> IO ()
|
||||
withIDEs_ miState f = void $ withIDEs miState $ fmap (, ()) . f
|
||||
|
||||
-- Stores the initialize messages sent by the client to be forwarded to SubIDEs when they are created.
|
||||
type InitParams = LSP.InitializeParams
|
||||
type InitParamsVar = MVar InitParams
|
||||
|
||||
-- Maps a packages unit id to its source file path, for all packages listed in a multi-package.yaml
|
||||
type MultiPackageYamlMapping = Map.Map String FilePath
|
||||
-- Maps a packages unit id to its source location, using PackageOnDisk for all packages in multi-package.yaml
|
||||
-- and PackageInDar for all known dars (currently extracted from data-dependencies)
|
||||
data PackageSourceLocation = PackageOnDisk PackageHome | PackageInDar DarFile deriving Show
|
||||
type MultiPackageYamlMapping = Map.Map UnitId PackageSourceLocation
|
||||
type MultiPackageYamlMappingVar = TMVar MultiPackageYamlMapping
|
||||
|
||||
-- Maps a dar path to the list of packages that directly depend on it
|
||||
type DarDependentPackages = Map.Map DarFile (Set.Set PackageHome)
|
||||
type DarDependentPackagesVar = TMVar DarDependentPackages
|
||||
|
||||
-- "Cache" for the home path of files/directories
|
||||
-- Cleared on daml.yaml modification and file deletion
|
||||
type SourceFileHomes = Map.Map FilePath PackageHome
|
||||
type SourceFileHomesVar = TMVar SourceFileHomes
|
||||
|
||||
data MultiIdeState = MultiIdeState
|
||||
{ fromClientMethodTrackerVar :: MethodTrackerVar 'LSP.FromClient
|
||||
@ -103,17 +202,38 @@ data MultiIdeState = MultiIdeState
|
||||
, subIDEsVar :: SubIDEsVar
|
||||
, initParamsVar :: InitParamsVar
|
||||
, toClientChan :: TChan BSL.ByteString
|
||||
, multiPackageMapping :: MultiPackageYamlMapping
|
||||
, debugPrint :: String -> IO ()
|
||||
, multiPackageMappingVar :: MultiPackageYamlMappingVar
|
||||
, darDependentPackagesVar :: DarDependentPackagesVar
|
||||
, logger :: Logger.Handle IO
|
||||
, multiPackageHome :: FilePath
|
||||
, defaultPackagePath :: PackageHome
|
||||
, sourceFileHomesVar :: SourceFileHomesVar
|
||||
, subIdeArgs :: [String]
|
||||
}
|
||||
|
||||
newMultiIdeState :: MultiPackageYamlMapping -> (String -> IO ()) -> IO MultiIdeState
|
||||
newMultiIdeState multiPackageMapping debugPrint = do
|
||||
logError :: MultiIdeState -> String -> IO ()
|
||||
logError miState msg = Logger.logError (logger miState) (T.pack msg)
|
||||
|
||||
logWarning :: MultiIdeState -> String -> IO ()
|
||||
logWarning miState msg = Logger.logWarning (logger miState) (T.pack msg)
|
||||
|
||||
logInfo :: MultiIdeState -> String -> IO ()
|
||||
logInfo miState msg = Logger.logInfo (logger miState) (T.pack msg)
|
||||
|
||||
logDebug :: MultiIdeState -> String -> IO ()
|
||||
logDebug miState msg = Logger.logDebug (logger miState) (T.pack msg)
|
||||
|
||||
newMultiIdeState :: FilePath -> PackageHome -> Logger.Priority -> [String] -> IO MultiIdeState
|
||||
newMultiIdeState multiPackageHome defaultPackagePath logThreshold subIdeArgs = do
|
||||
(fromClientMethodTrackerVar :: MethodTrackerVar 'LSP.FromClient) <- newTVarIO IM.emptyIxMap
|
||||
(fromServerMethodTrackerVar :: MethodTrackerVar 'LSP.FromServer) <- newTVarIO IM.emptyIxMap
|
||||
subIDEsVar <- newTMVarIO @SubIDEs mempty
|
||||
initParamsVar <- newEmptyMVar @InitParams
|
||||
toClientChan <- atomically newTChan
|
||||
multiPackageMappingVar <- newTMVarIO @MultiPackageYamlMapping mempty
|
||||
darDependentPackagesVar <- newTMVarIO @DarDependentPackages mempty
|
||||
sourceFileHomesVar <- newTMVarIO @SourceFileHomes mempty
|
||||
logger <- Logger.newStderrLogger logThreshold "Multi-IDE"
|
||||
pure MultiIdeState {..}
|
||||
|
||||
-- Forwarding
|
||||
@ -165,7 +285,7 @@ data Forwarding (m :: LSP.Method 'LSP.FromClient t) where
|
||||
. LSP.NotificationMessage m
|
||||
-> ForwardingBehaviour m
|
||||
-> Forwarding m
|
||||
ExplicitHandler
|
||||
ExplicitHandler
|
||||
:: ( (LSP.FromServerMessage -> IO ())
|
||||
-> (FilePath -> LSP.FromClientMessage -> IO ())
|
||||
-> IO ()
|
||||
@ -173,4 +293,9 @@ data Forwarding (m :: LSP.Method 'LSP.FromClient t) where
|
||||
-> Forwarding (m :: LSP.Method 'LSP.FromClient t)
|
||||
|
||||
type ResponseCombiner (m :: LSP.Method 'LSP.FromClient 'LSP.Request) =
|
||||
[(FilePath, Either LSP.ResponseError (LSP.ResponseResult m))] -> Either LSP.ResponseError (LSP.ResponseResult m)
|
||||
[(PackageHome, Either LSP.ResponseError (LSP.ResponseResult m))] -> Either LSP.ResponseError (LSP.ResponseResult m)
|
||||
|
||||
data SMethodWithSender (m :: LSP.Method 'LSP.FromServer t) = SMethodWithSender
|
||||
{ smsMethod :: LSP.SMethod m
|
||||
, smsSender :: Maybe PackageHome
|
||||
}
|
||||
|
@ -15,43 +15,48 @@ module DA.Cli.Damlc.Command.MultiIde.Util (
|
||||
|
||||
import Control.Concurrent.MVar
|
||||
import Control.Concurrent.STM.TMVar
|
||||
import Control.Exception (handle)
|
||||
import Control.Exception (SomeException, handle, try)
|
||||
import Control.Lens ((^.))
|
||||
import Control.Monad (void)
|
||||
import Control.Monad.STM
|
||||
import DA.Daml.Project.Config (readProjectConfig, queryProjectConfigRequired)
|
||||
import DA.Daml.Project.Types (ConfigError, ProjectPath (..))
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Control.Monad.Trans.Except
|
||||
import DA.Cli.Damlc.Command.MultiIde.Types
|
||||
import DA.Daml.Project.Config (readProjectConfig, queryProjectConfig, queryProjectConfigRequired)
|
||||
import DA.Daml.Project.Consts (projectConfigName)
|
||||
import DA.Daml.Project.Types (ConfigError)
|
||||
import Data.Aeson (Value (Null))
|
||||
import Data.Bifunctor (first)
|
||||
import Data.List.Extra (lower, replace)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import qualified Data.Text as T
|
||||
import qualified Language.LSP.Types as LSP
|
||||
import qualified Language.LSP.Types.Lens as LSP
|
||||
import qualified Language.LSP.Types.Capabilities as LSP
|
||||
import System.Directory (doesDirectoryExist, listDirectory)
|
||||
import System.FilePath (takeDirectory)
|
||||
import System.IO.Extra
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
-- Stop mangling my prints! >:(
|
||||
{-# ANN printLock ("HLint: ignore Avoid restricted function" :: String) #-}
|
||||
{-# NOINLINE printLock #-}
|
||||
printLock :: MVar ()
|
||||
printLock = unsafePerformIO $ newMVar ()
|
||||
|
||||
makeDebugPrint :: Bool -> String -> IO ()
|
||||
makeDebugPrint True msg = withMVar printLock $ \_ -> do
|
||||
hPutStrLn stderr msg
|
||||
hFlush stderr
|
||||
makeDebugPrint False _ = pure ()
|
||||
|
||||
infoPrint :: String -> IO ()
|
||||
infoPrint = makeDebugPrint True
|
||||
|
||||
warnPrint :: String -> IO ()
|
||||
warnPrint msg = infoPrint $ "Warning: " <> msg
|
||||
import System.Directory (doesDirectoryExist, listDirectory, withCurrentDirectory, canonicalizePath)
|
||||
import qualified System.FilePath as NativeFilePath
|
||||
import System.FilePath.Posix (joinDrive, takeDirectory, takeExtension)
|
||||
import System.IO (Handle, hClose, hFlush)
|
||||
|
||||
er :: Show x => String -> Either x a -> a
|
||||
er _msg (Right a) = a
|
||||
er msg (Left e) = error $ msg <> ": " <> show e
|
||||
|
||||
makeIOBlocker :: IO (IO a -> IO a, IO ())
|
||||
makeIOBlocker = do
|
||||
sendBlocker <- newEmptyMVar @()
|
||||
let unblock = putMVar sendBlocker ()
|
||||
onceUnblocked = (readMVar sendBlocker >>)
|
||||
pure (onceUnblocked, unblock)
|
||||
|
||||
modifyTMVar :: TMVar a -> (a -> a) -> STM ()
|
||||
modifyTMVar var f = do
|
||||
modifyTMVar var f = modifyTMVarM var (pure . f)
|
||||
|
||||
modifyTMVarM :: TMVar a -> (a -> STM a) -> STM ()
|
||||
modifyTMVarM var f = do
|
||||
x <- takeTMVar var
|
||||
putTMVar var (f x)
|
||||
x' <- f x
|
||||
putTMVar var x'
|
||||
|
||||
-- Taken directly from the Initialize response
|
||||
initializeResult :: LSP.InitializeResult
|
||||
@ -149,32 +154,146 @@ initializeResult = LSP.InitializeResult
|
||||
true = Just (LSP.InL True)
|
||||
false = Just (LSP.InL False)
|
||||
|
||||
initializeRequest :: InitParams -> SubIDEInstance -> LSP.FromClientMessage
|
||||
initializeRequest initParams ide = LSP.FromClientMess LSP.SInitialize LSP.RequestMessage
|
||||
{ _id = LSP.IdString $ ideMessageIdPrefix ide <> "-init"
|
||||
, _method = LSP.SInitialize
|
||||
, _params = initParams
|
||||
{ LSP._rootPath = Just $ T.pack $ unPackageHome $ ideHome ide
|
||||
, LSP._rootUri = Just $ LSP.filePathToUri $ unPackageHome $ ideHome ide
|
||||
}
|
||||
, _jsonrpc = "2.0"
|
||||
}
|
||||
|
||||
openFileNotification :: DamlFile -> T.Text -> LSP.FromClientMessage
|
||||
openFileNotification path content = LSP.FromClientMess LSP.STextDocumentDidOpen LSP.NotificationMessage
|
||||
{ _method = LSP.STextDocumentDidOpen
|
||||
, _params = LSP.DidOpenTextDocumentParams
|
||||
{ _textDocument = LSP.TextDocumentItem
|
||||
{ _uri = LSP.filePathToUri $ unDamlFile path
|
||||
, _languageId = "daml"
|
||||
, _version = 1
|
||||
, _text = content
|
||||
}
|
||||
}
|
||||
, _jsonrpc = "2.0"
|
||||
}
|
||||
|
||||
registerFileWatchersMessage :: LSP.RequestMessage 'LSP.ClientRegisterCapability
|
||||
registerFileWatchersMessage =
|
||||
LSP.RequestMessage "2.0" (LSP.IdString "MultiIdeWatchedFiles") LSP.SClientRegisterCapability $ LSP.RegistrationParams $ LSP.List
|
||||
[ LSP.SomeRegistration $ LSP.Registration "MultiIdeWatchedFiles" LSP.SWorkspaceDidChangeWatchedFiles $ LSP.DidChangeWatchedFilesRegistrationOptions $ LSP.List
|
||||
[ LSP.FileSystemWatcher "**/multi-package.yaml" Nothing
|
||||
, LSP.FileSystemWatcher "**/daml.yaml" Nothing
|
||||
, LSP.FileSystemWatcher "**/*.dar" Nothing
|
||||
, LSP.FileSystemWatcher "**/*.daml" Nothing
|
||||
]
|
||||
]
|
||||
|
||||
castLspId :: LSP.LspId m -> LSP.LspId m'
|
||||
castLspId (LSP.IdString s) = LSP.IdString s
|
||||
castLspId (LSP.IdInt i) = LSP.IdInt i
|
||||
|
||||
-- Given a file path, move up directory until we find a daml.yaml and give its path (if it exists)
|
||||
findHome :: FilePath -> IO (Maybe FilePath)
|
||||
findHome :: FilePath -> IO (Maybe PackageHome)
|
||||
findHome path = do
|
||||
exists <- doesDirectoryExist path
|
||||
if exists then aux path else aux (takeDirectory path)
|
||||
where
|
||||
aux :: FilePath -> IO (Maybe FilePath)
|
||||
aux :: FilePath -> IO (Maybe PackageHome)
|
||||
aux path = do
|
||||
hasDamlYaml <- elem "daml.yaml" <$> listDirectory path
|
||||
hasDamlYaml <- elem projectConfigName <$> listDirectory path
|
||||
if hasDamlYaml
|
||||
then pure $ Just path
|
||||
then pure $ Just $ PackageHome path
|
||||
else do
|
||||
let newPath = takeDirectory path
|
||||
if path == newPath
|
||||
then pure Nothing
|
||||
else aux newPath
|
||||
|
||||
unitIdFromDamlYaml :: FilePath -> IO (Either ConfigError String)
|
||||
unitIdFromDamlYaml path = do
|
||||
handle (\(e :: ConfigError) -> return $ Left e) $ do
|
||||
project <- readProjectConfig $ ProjectPath path
|
||||
pure $ do
|
||||
name <- queryProjectConfigRequired ["name"] project
|
||||
version <- queryProjectConfigRequired ["version"] project
|
||||
pure $ name <> "-" <> version
|
||||
unitIdAndDepsFromDamlYaml :: PackageHome -> IO (Either ConfigError (UnitId, [DarFile]))
|
||||
unitIdAndDepsFromDamlYaml path = do
|
||||
handle (\(e :: ConfigError) -> return $ Left e) $ runExceptT $ do
|
||||
project <- lift $ readProjectConfig $ toProjectPath path
|
||||
dataDeps <- except $ fromMaybe [] <$> queryProjectConfig ["data-dependencies"] project
|
||||
directDeps <- except $ fromMaybe [] <$> queryProjectConfig ["dependencies"] project
|
||||
let directDarDeps = filter (\dep -> takeExtension dep == ".dar") directDeps
|
||||
canonDeps <- lift $ withCurrentDirectory (unPackageHome path) $ traverse canonicalizePath $ dataDeps <> directDarDeps
|
||||
name <- except $ queryProjectConfigRequired ["name"] project
|
||||
version <- except $ queryProjectConfigRequired ["version"] project
|
||||
pure (UnitId $ name <> "-" <> version, DarFile . toPosixFilePath <$> canonDeps)
|
||||
|
||||
-- LSP requires all requests are replied to. When we don't have a working IDE (say the daml.yaml is malformed), we need to reply
|
||||
-- We don't want to reply with LSP errors, as there will be too many. Instead, we show our error in diagnostics, and send empty replies
|
||||
noIDEReply :: LSP.FromClientMessage -> Maybe LSP.FromServerMessage
|
||||
noIDEReply (LSP.FromClientMess method params) =
|
||||
case (method, params) of
|
||||
(LSP.STextDocumentWillSaveWaitUntil, _) -> makeRes params $ LSP.List []
|
||||
(LSP.STextDocumentCompletion, _) -> makeRes params $ LSP.InL $ LSP.List []
|
||||
(LSP.STextDocumentHover, _) -> makeRes params Nothing
|
||||
(LSP.STextDocumentSignatureHelp, _) -> makeRes params $ LSP.SignatureHelp (LSP.List []) Nothing Nothing
|
||||
(LSP.STextDocumentDeclaration, _) -> makeRes params $ LSP.InR $ LSP.InL $ LSP.List []
|
||||
(LSP.STextDocumentDefinition, _) -> makeRes params $ LSP.InR $ LSP.InL $ LSP.List []
|
||||
(LSP.STextDocumentDocumentSymbol, _) -> makeRes params $ LSP.InL $ LSP.List []
|
||||
(LSP.STextDocumentCodeAction, _) -> makeRes params $ LSP.List []
|
||||
(LSP.STextDocumentCodeLens, _) -> makeRes params $ LSP.List []
|
||||
(LSP.STextDocumentDocumentLink, _) -> makeRes params $ LSP.List []
|
||||
(LSP.STextDocumentColorPresentation, _) -> makeRes params $ LSP.List []
|
||||
(LSP.STextDocumentOnTypeFormatting, _) -> makeRes params $ LSP.List []
|
||||
(LSP.SWorkspaceExecuteCommand, _) -> makeRes params Null
|
||||
(LSP.SCustomMethod "daml/tryGetDefinition", LSP.ReqMess params) -> noDefinitionRes params
|
||||
(LSP.SCustomMethod "daml/gotoDefinitionByName", LSP.ReqMess params) -> noDefinitionRes params
|
||||
_ -> Nothing
|
||||
where
|
||||
makeRes :: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request). LSP.RequestMessage m -> LSP.ResponseResult m -> Maybe LSP.FromServerMessage
|
||||
makeRes params result = Just $ LSP.FromServerRsp (params ^. LSP.method) $ LSP.ResponseMessage "2.0" (Just $ params ^. LSP.id) (Right result)
|
||||
noDefinitionRes :: forall (m :: LSP.Method 'LSP.FromClient 'LSP.Request). LSP.RequestMessage m -> Maybe LSP.FromServerMessage
|
||||
noDefinitionRes params = Just $ LSP.FromServerRsp LSP.STextDocumentDefinition $ LSP.ResponseMessage "2.0" (Just $ castLspId $ params ^. LSP.id) $
|
||||
Right $ LSP.InR $ LSP.InL $ LSP.List []
|
||||
noIDEReply _ = Nothing
|
||||
|
||||
-- | Publishes an error diagnostic for a file containing the given message
|
||||
fullFileDiagnostic :: String -> FilePath -> LSP.FromServerMessage
|
||||
fullFileDiagnostic message path = LSP.FromServerMess LSP.STextDocumentPublishDiagnostics $ LSP.NotificationMessage "2.0" LSP.STextDocumentPublishDiagnostics
|
||||
$ LSP.PublishDiagnosticsParams (LSP.filePathToUri path) Nothing $ LSP.List [LSP.Diagnostic
|
||||
{ _range = LSP.Range (LSP.Position 0 0) (LSP.Position 0 1000)
|
||||
, _severity = Just LSP.DsError
|
||||
, _code = Nothing
|
||||
, _source = Just "Daml Multi-IDE"
|
||||
, _message = T.pack message
|
||||
, _tags = Nothing
|
||||
, _relatedInformation = Nothing
|
||||
}]
|
||||
|
||||
-- | Clears diagnostics for a given file
|
||||
clearDiagnostics :: FilePath -> LSP.FromServerMessage
|
||||
clearDiagnostics path = LSP.FromServerMess LSP.STextDocumentPublishDiagnostics $ LSP.NotificationMessage "2.0" LSP.STextDocumentPublishDiagnostics
|
||||
$ LSP.PublishDiagnosticsParams (LSP.filePathToUri path) Nothing $ LSP.List []
|
||||
|
||||
fromClientRequestLspId :: LSP.FromClientMessage -> Maybe LSP.SomeLspId
|
||||
fromClientRequestLspId (LSP.FromClientMess method params) =
|
||||
case (LSP.splitClientMethod method, params) of
|
||||
(LSP.IsClientReq, _) -> Just $ LSP.SomeLspId $ params ^. LSP.id
|
||||
(LSP.IsClientEither, LSP.ReqMess params) -> Just $ LSP.SomeLspId $ params ^. LSP.id
|
||||
_ -> Nothing
|
||||
fromClientRequestLspId _ = Nothing
|
||||
|
||||
fromClientRequestMethod :: LSP.FromClientMessage -> LSP.SomeMethod
|
||||
fromClientRequestMethod (LSP.FromClientMess method _) = LSP.SomeMethod method
|
||||
fromClientRequestMethod (LSP.FromClientRsp method _) = LSP.SomeMethod method
|
||||
|
||||
-- Windows can throw errors like `resource vanished` on dead handles, instead of being a no-op
|
||||
-- In those cases, we're already convinced the handle is closed, so we simply "try" to close handles
|
||||
-- and accept whatever happens
|
||||
hTryClose :: Handle -> IO ()
|
||||
hTryClose handle = void $ try @SomeException $ hClose handle
|
||||
|
||||
-- hFlush will error if the handle closes while its blocked on flushing
|
||||
-- We don't care what happens in this event, so we ignore the error as with tryClose
|
||||
hTryFlush :: Handle -> IO ()
|
||||
hTryFlush handle = void $ try @SomeException $ hFlush handle
|
||||
|
||||
-- Changes backslashes to forward slashes, lowercases the drive
|
||||
-- Need native filepath for splitDrive, as Posix version just takes first n `/`s
|
||||
toPosixFilePath :: FilePath -> FilePath
|
||||
toPosixFilePath = uncurry joinDrive . first lower . NativeFilePath.splitDrive . replace "\\" "/"
|
||||
|
@ -154,10 +154,6 @@ newtype InitPkgDb = InitPkgDb Bool
|
||||
initPkgDbOpt :: Parser InitPkgDb
|
||||
initPkgDbOpt = InitPkgDb <$> flagYesNoAuto "init-package-db" True "Initialize package database" idm
|
||||
|
||||
newtype MultiIdeVerbose = MultiIdeVerbose {getMultiIdeVerbose :: Bool}
|
||||
multiIdeVerboseOpt :: Parser MultiIdeVerbose
|
||||
multiIdeVerboseOpt = MultiIdeVerbose <$> flagYesNoAuto "verbose" False "Enable verbose logging for the Multi-IDE" idm
|
||||
|
||||
newtype EnableMultiPackage = EnableMultiPackage {getEnableMultiPackage :: Bool}
|
||||
enableMultiPackageOpt :: Parser EnableMultiPackage
|
||||
enableMultiPackageOpt = EnableMultiPackage <$> flagYesNoAuto "enable-multi-package" True "Enable/disable multi-package.yaml support (enabled by default)" idm
|
||||
|
Loading…
Reference in New Issue
Block a user