mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-05 00:15:32 +00:00
Merge pull request #13156 from keymanapp/feat/developer/13134-improve-messages-and-links
feat(developer): improve compiler messages and user interface
This commit is contained in:
commit
2d5e3ee440
13 changed files with 373 additions and 106 deletions
|
|
@ -92,6 +92,8 @@ function MakeAPIURL(path: string): string;
|
|||
|
||||
function MakeKeymanURL(const path: string): string;
|
||||
|
||||
function URL_KmcMessage(const id: string): string;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
|
|
@ -108,6 +110,7 @@ const
|
|||
S_UserAgent_Developer = 'Keyman Developer';
|
||||
S_UserAgent_Diagnostics = 'Keyman for Windows Diagnostics';
|
||||
|
||||
S_Host_KmnSh = 'https://kmn.sh';
|
||||
S_KeymanCom = 'https://keyman.com';
|
||||
S_APIProtocol = 'https';
|
||||
S_APIServer = 'api.keyman.com';
|
||||
|
|
@ -120,6 +123,7 @@ const
|
|||
|
||||
const
|
||||
URLPath_PackageDownload_Format = '/go/package/download/%0:s?platform=windows&tier=%1:s&bcp47=%2:s&update=%3:d';
|
||||
URL_KeymanDeveloper_HelpKmcMessage_Format = S_Host_KmnSh+'/%0:s';
|
||||
|
||||
function API_UserAgent: string;
|
||||
begin
|
||||
|
|
@ -136,7 +140,6 @@ begin
|
|||
Result := S_UserAgent_Diagnostics + '/' + GetVersionString;
|
||||
end;
|
||||
|
||||
|
||||
function MakeKeymanURL(const path: string): string;
|
||||
begin
|
||||
Result := KeymanCom_Protocol_Server + path;
|
||||
|
|
@ -179,4 +182,9 @@ begin
|
|||
Result := Format(URLPath_PackageDownload_Format, [URLEncode(PackageID), URLEncode(CKeymanVersionInfo.Tier), URLEncode(BCP47), IsUpdateInt]);
|
||||
end;
|
||||
|
||||
function URL_KmcMessage(const id: string): string;
|
||||
begin
|
||||
Result := Format(URL_KeymanDeveloper_HelpKmcMessage_Format, [id.ToLower]);
|
||||
end;
|
||||
|
||||
end.
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ uses
|
|||
Winapi.Windows;
|
||||
|
||||
function GetVersionString: WideString;
|
||||
function GetMajorMinorVersionString: string;
|
||||
procedure GetVersionBuild(var vMajor, vMinor: Integer);
|
||||
function GetVersionCopyright: WideString;
|
||||
function GetFileVersionString(const FileName: WideString): WideString;
|
||||
|
|
@ -54,6 +55,12 @@ begin
|
|||
IntToStr(LoWord(VersionMinor));
|
||||
end;
|
||||
|
||||
function GetMajorMinorVersionString: string;
|
||||
begin
|
||||
Result := IntToStr(HiWord(VersionMajor)) + '.' + // I3310
|
||||
IntToStr(LoWord(VersionMajor));
|
||||
end;
|
||||
|
||||
procedure GetVersionBuild(var vMajor, vMinor: Integer);
|
||||
begin
|
||||
vMajor := VersionMajor;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CompilerError, CompilerErrorMask } from '@keymanapp/developer-utils';
|
||||
import { CompilerError, CompilerErrorMask, CompilerEvent, dedentCompilerMessageDetail } from '@keymanapp/developer-utils';
|
||||
import {assert, expect} from 'chai';
|
||||
|
||||
//
|
||||
|
|
@ -37,8 +37,24 @@ export function verifyCompilerMessagesObject(source: Record<string,any>, namespa
|
|||
const c = o[1].toUpperCase() + '_' + o[2];
|
||||
expect(m[c]).to.be.a('number', `Expected constant name ${c} to exist`);
|
||||
|
||||
const v = m[key]('','','','','','','','','','','','' /* ignore arguments*/);
|
||||
const v: CompilerEvent = m[key]('','','','','','','','','','','','' /* ignore arguments*/);
|
||||
expect(v.code).to.equal(m[c], `Function ${key} returns the wrong code`);
|
||||
|
||||
assert.isNotNull(v.message, `Function ${key} returns an invalid message: '${v.message}'`);
|
||||
assert.isNotEmpty(v.message, `Function ${key} returns an empty message: '${v.message}'`);
|
||||
|
||||
// Verify layout of compiler detail lines
|
||||
|
||||
if(v.detail) {
|
||||
const detail = dedentCompilerMessageDetail(v).split('\n');
|
||||
for(const line of detail) {
|
||||
// Prevent lines over 80 characters -- we don't rewrap at this stage
|
||||
// Don't check lines with URLs because they may be forced to go long
|
||||
if(!line.includes('http')) {
|
||||
assert.isAtMost(line.length, 80, `Function ${key} returns a message detail with at least one line longer than 80 characters: '${line}'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(typeof m[key] == 'number') {
|
||||
const o = /^(DEBUG|VERBOSE|INFO|HINT|WARN|ERROR|FATAL)_([A-Za-z0-9_]+)$/.exec(key);
|
||||
|
|
|
|||
|
|
@ -399,6 +399,19 @@ export const CompilerMessageSpec = (code: number, message: string, detail?: stri
|
|||
detail,
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove initial whitespace from compiler detail messages, to enable
|
||||
* indented formatting of message detail strings inside the message
|
||||
* definitions
|
||||
* @param event
|
||||
* @returns dedented event detail
|
||||
*/
|
||||
export function dedentCompilerMessageDetail(event: CompilerEvent) {
|
||||
// TODO(lowpri): dedent may be too naive -- should use least
|
||||
// non-zero whitespace line as amount to dedent
|
||||
return (event.detail ?? '').replace(/^[ ]+/gm, '');
|
||||
}
|
||||
|
||||
export const CompilerMessageDef = (param: any) => String(param ?? `<param>`);
|
||||
|
||||
export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerE
|
|||
ALL_COMPILER_LOG_FORMATS, CompilerLogFormat,
|
||||
CompilerMessageOverride,
|
||||
CompilerMessageOverrideMap,
|
||||
|
||||
dedentCompilerMessageDetail,
|
||||
KeymanCompilerArtifact,
|
||||
KeymanCompilerArtifactOptional,
|
||||
KeymanCompilerArtifacts,
|
||||
|
|
|
|||
|
|
@ -19,9 +19,10 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
|
|||
*/
|
||||
export class CopierMessages {
|
||||
static FATAL_UnexpectedException = SevFatal | 0x0001;
|
||||
static Fatal_UnexpectedException = (o:{e: any}) => m(
|
||||
static Fatal_UnexpectedException = (o:{e: any}) => CompilerMessageSpecWithException(
|
||||
this.FATAL_UnexpectedException,
|
||||
null, o.e ?? 'unknown error'
|
||||
null,
|
||||
o.e ?? 'unknown error'
|
||||
);
|
||||
|
||||
static INFO_CopyingProject = SevInfo | 0x0002;
|
||||
|
|
@ -123,81 +124,90 @@ export class CopierMessages {
|
|||
static ERROR_CannotDownloadFolderFromGitHub = SevError | 0x0012;
|
||||
static Error_CannotDownloadFolderFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m(
|
||||
this.ERROR_CannotDownloadFolderFromGitHub,
|
||||
`The folder '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`,
|
||||
`An error was encountered attempting to download a folder from GitHub API. Check the
|
||||
provided error details for the cause.`
|
||||
);
|
||||
`The folder '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, `
|
||||
An error was encountered attempting to download a folder from GitHub API. Check
|
||||
the provided error details for the cause.
|
||||
`);
|
||||
|
||||
static ERROR_FolderDownloadedFromGitHubIsNotAValidFolder = SevError | 0x0013;
|
||||
static Error_FolderDownloadedFromGitHubIsNotAValidFolder = (o:{ref: string}) => m(
|
||||
this.ERROR_FolderDownloadedFromGitHubIsNotAValidFolder,
|
||||
`The path '${def(o.ref)}' does not appear to be a folder on GitHub`,
|
||||
`The provided path may be a file or may not exist. Check the reference
|
||||
before trying again.`
|
||||
);
|
||||
`The path '${def(o.ref)}' does not appear to be a folder on GitHub`, `
|
||||
The provided path may be a file or may not exist. Check the reference
|
||||
before trying again.
|
||||
`);
|
||||
|
||||
static WARN_CannotDownloadFileFromGitHub = SevWarn | 0x0014;
|
||||
static Warn_CannotDownloadFileFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m(
|
||||
this.WARN_CannotDownloadFileFromGitHub,
|
||||
`The file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`,
|
||||
`An error was encountered attempting to download a file from GitHub. Check the
|
||||
provided error details for the cause.`
|
||||
);
|
||||
`The file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`, `
|
||||
An error was encountered attempting to download a file from GitHub. Check the
|
||||
provided error details for the cause.
|
||||
`);
|
||||
|
||||
static ERROR_InvalidCloudKeyboardId = SevError | 0x0015;
|
||||
static Error_InvalidCloudKeyboardId = (o:{id: string}) => m(
|
||||
this.ERROR_InvalidCloudKeyboardId,
|
||||
`The keyboard identifier '${def(o.id)}' is not a valid keyboard identifier`,
|
||||
`Keyboard identifiers on Keyman Cloud can only use the characters a-z, 0-9, and _.`
|
||||
);
|
||||
`The keyboard identifier '${def(o.id)}' is not a valid keyboard identifier`, `
|
||||
Keyboard identifiers on Keyman Cloud can only use the characters
|
||||
\`a\`-\`z\`, \`0\`-\`9\`, and \`_\`.
|
||||
`);
|
||||
|
||||
static ERROR_CouldNotRetrieveFromCloud = SevError | 0x0016;
|
||||
static Error_CouldNotRetrieveFromCloud = (o:{id: string, message?: string, cause?: string}) => m(
|
||||
this.ERROR_CouldNotRetrieveFromCloud,
|
||||
`Details for keyboard or model identified by '${def(o.id)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}`,
|
||||
`An error was encountered attempting to retrieve keyboard or model details from Keyman Cloud API. Check the
|
||||
provided error details for the cause.`
|
||||
);
|
||||
`Details for keyboard or model identified by '${def(o.id)}' could not be `+
|
||||
`downloaded: ${def(o.message)} ${def(o.cause)}`, `
|
||||
An error was encountered attempting to retrieve keyboard or model details from
|
||||
Keyman Cloud API. Check the provided error details for the cause.
|
||||
`);
|
||||
|
||||
static ERROR_KeymanCloudReturnedInvalidData = SevError | 0x0017;
|
||||
static Error_KeymanCloudReturnedInvalidData = (o:{id: string}) => m(
|
||||
this.ERROR_KeymanCloudReturnedInvalidData,
|
||||
`Keyman Cloud API returned invalid data for keyboard or model identified by '${def(o.id)}'`,
|
||||
`There may be a network error or a server error. Retry your request later or contact
|
||||
Keyman Support for assistance.`
|
||||
);
|
||||
`Keyman Cloud API returned invalid data for keyboard or model identified by '${def(o.id)}'`, `
|
||||
There may be a network error or a server error. Retry your request later or
|
||||
contact Keyman Support for assistance.
|
||||
`);
|
||||
|
||||
static ERROR_CloudDoesNotHaveSource = SevError | 0x0018;
|
||||
static Error_CloudDoesNotHaveSource = (o:{id: string}) => m(
|
||||
this.ERROR_CloudDoesNotHaveSource,
|
||||
`The keyboard or model identified by '${def(o.id)}' does not have source available`,
|
||||
`Legacy keyboards in Keyman Cloud do not have source available. Check the Keyman keyboard catalog
|
||||
at keyman.com for further details. Some new keyboards or models may be available as binary-only.`
|
||||
);
|
||||
`The keyboard or model identified by '${def(o.id)}' does not have source available`, `
|
||||
Legacy keyboards in Keyman Cloud do not have source available. Check the Keyman
|
||||
keyboard catalog at https://keyman.com/keyboards for further details. Some new
|
||||
keyboards or models may be available as binary-only.
|
||||
`);
|
||||
|
||||
static ERROR_CannotDownloadRepoFromGitHub = SevError | 0x0019 ;
|
||||
static Error_CannotDownloadRepoFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m(
|
||||
this.ERROR_CannotDownloadRepoFromGitHub,
|
||||
`The repository at '${def(o.ref)}' could not be accessed: ${def(o.message)} ${def(o.cause)}`,
|
||||
`An error was encountered attempting to download details about a repository from GitHub API.
|
||||
Check the provided error details for the cause.`
|
||||
);
|
||||
`The repository at '${def(o.ref)}' could not be accessed: ${def(o.message)} ${def(o.cause)}`, `
|
||||
An error was encountered attempting to download details about a repository
|
||||
from GitHub API. Check the provided error details for the cause.
|
||||
`);
|
||||
|
||||
//------------------------------------------------------------------------------|
|
||||
// max length of detail message lines (checked by verifyCompilerMessagesObject) |
|
||||
//------------------------------------------------------------------------------|
|
||||
|
||||
static ERROR_CouldNotFindDefaultBranchOnGitHub = SevError | 0x001A;
|
||||
static Error_CouldNotFindDefaultBranchOnGitHub = (o:{ref: string}) => m(
|
||||
this.ERROR_CouldNotFindDefaultBranchOnGitHub,
|
||||
`The default branch could not be found for the GitHub repository '${def(o.ref)}'`,
|
||||
`The repository may be private, or you may have a typo in the owner or repository name.`
|
||||
);
|
||||
`The default branch could not be found for the GitHub repository '${def(o.ref)}'`, `
|
||||
The repository may be private, or you may have a typo in the owner or
|
||||
repository name.
|
||||
`);
|
||||
|
||||
static INFO_CannotDownloadBinaryFileFromGitHub = SevInfo | 0x001B;
|
||||
static Info_CannotDownloadBinaryFileFromGitHub = (o:{ref: string, message?: string, cause?: string}) => m(
|
||||
this.INFO_CannotDownloadBinaryFileFromGitHub,
|
||||
`The Keyman binary file '${def(o.ref)}' could not be downloaded: ${def(o.message)} ${def(o.cause)}. This is not normally a problem`,
|
||||
`In most repositories, Keyman binary files such as .kmx, .kmp, .js are not included.
|
||||
This is not normally a problem, as the files can be built from the source. Check
|
||||
the provided error details for more details.`
|
||||
);
|
||||
`The Keyman binary file '${def(o.ref)}' could not be downloaded: ${def(o.message)} `+
|
||||
`${def(o.cause)}. This is not normally a problem`, `
|
||||
In most repositories, Keyman binary files such as .kmx, .kmp, .js are not
|
||||
included. This is not normally a problem, as the files can be built from the
|
||||
source. Check the provided error details for more details.
|
||||
`);
|
||||
|
||||
static VERBOSE_DownloadingFile = SevVerbose | 0x001C;
|
||||
static Verbose_DownloadingFile = (o:{filename: string, url: string}) => m(
|
||||
|
|
|
|||
|
|
@ -18,8 +18,9 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
|
|||
*/
|
||||
export class GeneratorMessages {
|
||||
static FATAL_UnexpectedException = SevFatal | 0x0001;
|
||||
static Fatal_UnexpectedException = (o:{e: any}) => m(
|
||||
this.FATAL_UnexpectedException, null, o.e ?? 'unknown error'
|
||||
static Fatal_UnexpectedException = (o:{e: any}) => CompilerMessageSpecWithException(
|
||||
this.FATAL_UnexpectedException, null,
|
||||
o.e ?? 'unknown error',
|
||||
);
|
||||
|
||||
static INFO_GeneratingProject = SevInfo | 0x0002;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { keyAddress, KmnCompilerMessages } from "../compiler/kmn-compiler-messages.js";
|
||||
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageDef as def, CompilerMessageSpec } from "@keymanapp/developer-utils";
|
||||
import { kmnfile } from "./compiler-globals.js";
|
||||
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageDef as def, CompilerMessageSpec as m } from "@keymanapp/developer-utils";
|
||||
import { KeyAddress } from "./validate-layout-file.js";
|
||||
|
||||
const Namespace = CompilerErrorNamespace.KmwCompiler;
|
||||
|
|
@ -10,12 +9,6 @@ const SevHint = CompilerErrorSeverity.Hint | Namespace;
|
|||
const SevError = CompilerErrorSeverity.Error | Namespace;
|
||||
// const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
|
||||
|
||||
const m = (code: number, message: string, o?: {filename?: string, line?: number}) : CompilerEvent => ({
|
||||
...CompilerMessageSpec(code, message),
|
||||
filename: o?.filename ?? kmnfile,
|
||||
line: o?.line,
|
||||
});
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Error messages reported by the KeymanWeb .kmn compiler.
|
||||
|
|
@ -34,25 +27,62 @@ export class KmwCompilerMessages extends KmnCompilerMessages {
|
|||
// 0x0001 - Reserved for removed identifier ERROR_NotAnyRequiresVersion14 =
|
||||
// SevError | 0x0001, added in 17.0, removed in 18.0
|
||||
|
||||
//------------------------------------------------------------------------------|
|
||||
// max length of detail message lines (checked by verifyCompilerMessagesObject) |
|
||||
//------------------------------------------------------------------------------|
|
||||
|
||||
static ERROR_TouchLayoutIdentifierRequires15 = SevError | 0x0002;
|
||||
static Error_TouchLayoutIdentifierRequires15 = (o:{keyId:string, platformName:string, layerId:string, address:KeyAddress}) => m(this.ERROR_TouchLayoutIdentifierRequires15,
|
||||
`Key "${def(o.keyId)}" on "${def(o.platformName)}", layer "${def(o.layerId)}" (${keyAddress(o.address)}) has a multi-part identifier which requires version 15.0 or newer.`);
|
||||
static Error_TouchLayoutIdentifierRequires15 = (o:{keyId:string, platformName:string, layerId:string, address:KeyAddress}) => m(
|
||||
this.ERROR_TouchLayoutIdentifierRequires15,
|
||||
`Key "${def(o.keyId)}" on "${def(o.platformName)}", layer "${def(o.layerId)}" (${keyAddress(o.address)}) has a `+
|
||||
`multi-part identifier which requires version 15.0 or newer`, `
|
||||
The Unicode key format \`U_xxxx_yyyy\` is supported in Keyman 15.0 and later
|
||||
versions.
|
||||
|
||||
For example, \`U_0041_0300\` means the key will by default emit
|
||||
\`U+0041 U+0300\` (À); earlier versions allow only a single Unicode value in
|
||||
the identifier, e.g. \`U_0300\`.
|
||||
`);
|
||||
|
||||
static ERROR_InvalidTouchLayoutFileFormat = SevError | 0x0003;
|
||||
static Error_InvalidTouchLayoutFileFormat = (o:{msg: string}) => m(this.ERROR_InvalidTouchLayoutFileFormat,
|
||||
`Invalid touch layout file: ${def(o.msg)}`);
|
||||
static Error_InvalidTouchLayoutFileFormat = (o:{msg: string}) => m(
|
||||
this.ERROR_InvalidTouchLayoutFileFormat,
|
||||
`Invalid touch layout file: ${def(o.msg)}`, `
|
||||
The referenced .keyman-touch-layout file contained invalid JSON content. The
|
||||
touch layout file format is documented at
|
||||
https://help.keyman.com/developer/current-version/reference/file-types/keyman-touch-layout.
|
||||
`);
|
||||
|
||||
static ERROR_TouchLayoutFileDoesNotExist = SevError | 0x0004;
|
||||
static Error_TouchLayoutFileDoesNotExist = (o:{filename:string}) => m(this.ERROR_TouchLayoutFileDoesNotExist,
|
||||
`Touch layout file ${def(o.filename)} does not exist`);
|
||||
static Error_TouchLayoutFileDoesNotExist = (o:{filename:string}) => m(
|
||||
this.ERROR_TouchLayoutFileDoesNotExist,
|
||||
`Touch layout file ${def(o.filename)} does not exist`, `
|
||||
The compiler was unable to load the referenced .keyman-touch-layout file.
|
||||
Verify that the referenced file does exist and is accessible to the compiler.
|
||||
`);
|
||||
|
||||
static HINT_TouchLayoutUsesUnsupportedGesturesDownlevel = SevHint | 0x0005;
|
||||
static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m(this.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel,
|
||||
`The touch layout uses a flick or multi-tap gesture on key ${def(o.keyId)}, which is only available on version 17.0+ of Keyman`);
|
||||
static Hint_TouchLayoutUsesUnsupportedGesturesDownlevel = (o:{keyId:string}) => m(
|
||||
this.HINT_TouchLayoutUsesUnsupportedGesturesDownlevel,
|
||||
`The touch layout uses a flick or multi-tap gesture on key ${def(o.keyId)}, which `+
|
||||
`is only available on version 17.0+ of Keyman`, `
|
||||
Flick and multi-tap gesture support was added to Keyman mobile platforms in
|
||||
version 17.0. Keyboards which include these gestures can still work in earlier
|
||||
versions of Keyman, but any flick and multi-tap gestures will not be available,
|
||||
which may make some characters inaccesssible.
|
||||
`);
|
||||
|
||||
static INFO_MinimumWebEngineVersion = SevInfo | 0x0006;
|
||||
static Info_MinimumWebEngineVersion = (o:{version:string}) => m(
|
||||
this.INFO_MinimumWebEngineVersion,
|
||||
`The compiler has assigned a minimum web engine version of ${o.version} based on features used in this keyboard`
|
||||
);
|
||||
`The compiler has assigned a minimum web engine version of ${o.version} based on `+
|
||||
`features used in this keyboard`, `
|
||||
If the [\`&version\` store](https://help.keyman.com/developer/language/reference/version)
|
||||
is not present in the keyboard source, the compiler attempts to assign the
|
||||
lowest possible Keyman version that can support the features found in the
|
||||
keyboard. Details on the history of language features and supported versions
|
||||
can be found at https://help.keyman.com/developer/language/guide/history.
|
||||
`);
|
||||
|
||||
//------------------------------------------------------------------------------|
|
||||
};
|
||||
|
|
|
|||
|
|
@ -217,33 +217,45 @@ export class LdmlCompilerMessages {
|
|||
static Error_UnparseableTransformFrom = (o: { from: string, message: string }) =>
|
||||
m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`);
|
||||
|
||||
//------------------------------------------------------------------------------|
|
||||
// max length of detail message lines (checked by verifyCompilerMessagesObject) |
|
||||
//------------------------------------------------------------------------------|
|
||||
|
||||
static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01;
|
||||
static Error_IllegalTransformDollarsign = (o: { from: string }) => m(
|
||||
this.ERROR_IllegalTransformDollarsign,
|
||||
`Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`,
|
||||
`**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a variable name, `+
|
||||
`the variable name may not be valid (A-Z, a-z, 0-9, _, 32 character maximum).`
|
||||
);
|
||||
`Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, `
|
||||
**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a
|
||||
variable name, the variable name may not be valid (A-Z, a-z, 0-9, _, 32
|
||||
character maximum).
|
||||
`);
|
||||
|
||||
static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02;
|
||||
static Error_TransformFromMatchesNothing = (o: { from: string }) =>
|
||||
m(this.ERROR_TransformFromMatchesNothing, `Invalid transfom from="${def(o.from)}": Matches an empty string.`);
|
||||
static Error_TransformFromMatchesNothing = (o: { from: string }) => m(
|
||||
this.ERROR_TransformFromMatchesNothing,
|
||||
`Invalid transfom from="${def(o.from)}": Matches an empty string.`
|
||||
);
|
||||
|
||||
static ERROR_IllegalTransformPlus = SevErrorTransform | 0x03;
|
||||
static Error_IllegalTransformPlus = (o: { from: string }) =>
|
||||
m(this.ERROR_IllegalTransformPlus, `Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`,
|
||||
'**Hint**: Use `\\+` to match a literal plus.');
|
||||
static Error_IllegalTransformPlus = (o: { from: string }) => m(
|
||||
this.ERROR_IllegalTransformPlus,
|
||||
`Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`, `
|
||||
**Hint**: Use \`\\+\` to match a literal plus.
|
||||
`);
|
||||
|
||||
static ERROR_IllegalTransformAsterisk = SevErrorTransform | 0x04;
|
||||
static Error_IllegalTransformAsterisk = (o: { from: string }) =>
|
||||
m(this.ERROR_IllegalTransformAsterisk, `Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`,
|
||||
'**Hint**: Use `\\*` to match a literal asterisk.');
|
||||
static Error_IllegalTransformAsterisk = (o: { from: string }) =>m(
|
||||
this.ERROR_IllegalTransformAsterisk,
|
||||
`Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`, `
|
||||
**Hint**: Use \`\\*\` to match a literal asterisk.
|
||||
`);
|
||||
|
||||
static ERROR_IllegalTransformToUset = SevErrorTransform | 0x05;
|
||||
static Error_IllegalTransformToUset = (o: { to: string }) => m(
|
||||
this.ERROR_IllegalTransformToUset,
|
||||
`Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`,
|
||||
'**Hint**: If a map was meant, must use the form `<transform from="($[fromSet])" to="$[1:toSet]"/>`.'
|
||||
);
|
||||
`Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`, `
|
||||
**Hint**: If a map was meant, must use the form
|
||||
\`<transform from="($[fromSet])" to="$[1:toSet]"/>\`.
|
||||
`);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as fs from 'fs';
|
|||
import * as path from 'path';
|
||||
|
||||
import { Command, Option } from 'commander';
|
||||
import { escapeMarkdownChar, KeymanUrls, CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerEvent } from '@keymanapp/developer-utils';
|
||||
import { escapeMarkdownChar, KeymanUrls, CompilerBaseOptions, CompilerCallbacks, CompilerError, CompilerErrorNamespace, CompilerEvent, dedentCompilerMessageDetail } from '@keymanapp/developer-utils';
|
||||
import { CompilerMessageSource, messageNamespaceKeys, messageSources } from '../messages/messageNamespaces.js';
|
||||
import { NodeCompilerCallbacks } from '../util/NodeCompilerCallbacks.js';
|
||||
import { exitProcess } from '../util/sysexits.js';
|
||||
|
|
@ -189,7 +189,7 @@ function getMessageDetail(cls: any, id: string, escapeMarkdown: boolean): Compil
|
|||
throw new Error(`Call to ${cls.name}.${f} returned null`);
|
||||
}
|
||||
|
||||
event.detail = (event.detail ?? '').replace(/^[ ]+/gm, ''); // TODO(lowpri): dedent may be too naive?
|
||||
event.detail = dedentCompilerMessageDetail(event);
|
||||
event.message = event.message ?? '';
|
||||
event.message = event?.exceptionVar
|
||||
? 'This is an internal error; the message will vary'
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ inherited frmMessages: TfrmMessages
|
|||
Font.Name = 'Courier New'
|
||||
Font.Style = []
|
||||
ParentFont = False
|
||||
PopupMenu = mnuPopup
|
||||
ReadOnly = True
|
||||
ScrollBars = ssBoth
|
||||
TabOrder = 0
|
||||
|
|
@ -35,6 +34,8 @@ inherited frmMessages: TfrmMessages
|
|||
OnClick = memoMessageClick
|
||||
OnDblClick = memoMessageDblClick
|
||||
OnKeyDown = memoMessageKeyDown
|
||||
OnMouseDown = memoMessageMouseDown
|
||||
OnMouseMove = memoMessageMouseMove
|
||||
end
|
||||
object dlgSave: TSaveDialog
|
||||
DefaultExt = 'txt'
|
||||
|
|
@ -51,6 +52,10 @@ inherited frmMessages: TfrmMessages
|
|||
Default = True
|
||||
OnClick = mnuViewItemClick
|
||||
end
|
||||
object mnuOpenDocumentation: TMenuItem
|
||||
Caption = 'Open &documentation on selected message'
|
||||
OnClick = mnuOpenDocumentationClick
|
||||
end
|
||||
object mnuSeparator1: TMenuItem
|
||||
Caption = '-'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ interface
|
|||
|
||||
uses
|
||||
System.Classes,
|
||||
System.Contnrs,
|
||||
System.Generics.Collections,
|
||||
System.SysUtils,
|
||||
Vcl.Controls,
|
||||
Vcl.Dialogs,
|
||||
|
|
@ -54,12 +54,24 @@ uses
|
|||
UfrmTike, Vcl.ComCtrls;
|
||||
|
||||
type
|
||||
TMessageItemSegment = record
|
||||
text: string;
|
||||
color: TColor;
|
||||
underline: Boolean;
|
||||
index: Integer;
|
||||
end;
|
||||
|
||||
TMessageItem = class
|
||||
FileName: string;
|
||||
Msg: string;
|
||||
MsgCode, Line: Integer;
|
||||
|
||||
Segments: TArray<TMessageItemSegment>;
|
||||
LineLength: Integer;
|
||||
end;
|
||||
|
||||
TMessageItemList = class(TObjectList<TMessageItem>);
|
||||
|
||||
TfrmMessages = class(TTikeDockForm)
|
||||
dlgSave: TSaveDialog;
|
||||
memoMessage: TRichEdit;
|
||||
|
|
@ -71,6 +83,7 @@ type
|
|||
cmdmNextMessage: TMenuItem;
|
||||
mnuViewItem: TMenuItem;
|
||||
mnuSeparator1: TMenuItem;
|
||||
mnuOpenDocumentation: TMenuItem;
|
||||
procedure memoMessageDblClick(Sender: TObject);
|
||||
procedure cmdmSaveToFileClick(Sender: TObject);
|
||||
procedure cmdmClearClick(Sender: TObject);
|
||||
|
|
@ -83,8 +96,13 @@ type
|
|||
procedure mnuPopupPopup(Sender: TObject);
|
||||
procedure mnuViewItemClick(Sender: TObject);
|
||||
procedure FormDestroy(Sender: TObject);
|
||||
procedure memoMessageMouseMove(Sender: TObject; Shift: TShiftState; X,
|
||||
Y: Integer);
|
||||
procedure mnuOpenDocumentationClick(Sender: TObject);
|
||||
procedure memoMessageMouseDown(Sender: TObject; Button: TMouseButton;
|
||||
Shift: TShiftState; X, Y: Integer);
|
||||
private
|
||||
FMessageItems: TObjectList;
|
||||
FMessageItems: TMessageItemList;
|
||||
function GetSelLine: Integer;
|
||||
procedure SetSelLine(Value: Integer);
|
||||
protected
|
||||
|
|
@ -107,6 +125,8 @@ var
|
|||
implementation
|
||||
|
||||
uses
|
||||
System.RegularExpressions,
|
||||
System.Types,
|
||||
Winapi.RichEdit,
|
||||
|
||||
UfrmMain,
|
||||
|
|
@ -116,7 +136,9 @@ uses
|
|||
Keyman.Developer.System.Project.Project,
|
||||
Keyman.Developer.System.Project.ProjectFile,
|
||||
Keyman.Developer.UI.Project.ProjectFileUI,
|
||||
Keyman.Developer.UI.Project.ProjectUI;
|
||||
Keyman.Developer.UI.Project.ProjectUI,
|
||||
Upload_Settings,
|
||||
utilexecute;
|
||||
|
||||
{$R *.DFM}
|
||||
|
||||
|
|
@ -149,24 +171,29 @@ const // ABGR - text on white bg, Colors similar to Light+ VSCode color theme
|
|||
INFO_ProjectBuiltSuccessfully = NAMESPACE_Infrastructure or $000B;
|
||||
INFO_ProjectNotBuiltSuccessfully = NAMESPACE_Infrastructure or $000C;
|
||||
|
||||
Segment_Filename = 0;
|
||||
Segment_Filename_Separator = 1;
|
||||
Segment_Line = 2;
|
||||
Segment_Line_Separator = 3;
|
||||
Segment_State = 4;
|
||||
Segment_Code = 5;
|
||||
Segment_Code_Separator = 6;
|
||||
Segment_Message = 7;
|
||||
|
||||
procedure TfrmMessages.Add(state: TProjectLogState; filename, msg: WideString; MsgCode, line: Integer);
|
||||
type
|
||||
TSegment = record
|
||||
text: string;
|
||||
color: TColor;
|
||||
end;
|
||||
var
|
||||
mi: TMessageItem;
|
||||
FColor: TColor;
|
||||
FTextColor: TColor;
|
||||
Segments: array of TSegment;
|
||||
Segments: TArray<TMessageItemSegment>;
|
||||
i, x: Integer;
|
||||
|
||||
procedure AddText(const text: string; color: TColor);
|
||||
procedure AddText(const text: string; color: TColor; underline: Boolean = false);
|
||||
var
|
||||
s: TSegment;
|
||||
s: TMessageItemSegment;
|
||||
begin
|
||||
s.text := text;
|
||||
s.underline := underline;
|
||||
s.color := color;
|
||||
SetLength(Segments, Length(Segments)+1);
|
||||
Segments[High(Segments)] := s;
|
||||
|
|
@ -175,7 +202,7 @@ var
|
|||
procedure AddLine;
|
||||
var
|
||||
t: string;
|
||||
s: TSegment;
|
||||
s: TMessageItemSegment;
|
||||
cr: TCharRange;
|
||||
cf: TCharFormat;
|
||||
gtle: TGetTextLengthEx;
|
||||
|
|
@ -184,7 +211,7 @@ var
|
|||
// use direct messages to the RichEdit control to emit formatted text
|
||||
FillChar(cf, sizeof(TCharFormat), 0);
|
||||
cf.cbSize := sizeof(TCharFormat);
|
||||
cf.dwMask := CFM_COLOR;
|
||||
cf.dwMask := CFM_COLOR or CFM_UNDERLINE;
|
||||
|
||||
gtle.flags := GTL_PRECISE or GTL_NUMCHARS;
|
||||
gtle.codepage := 1200;
|
||||
|
|
@ -198,10 +225,13 @@ var
|
|||
|
||||
for s in Segments do
|
||||
begin
|
||||
if s.Text.Length = 0 then
|
||||
Continue;
|
||||
cr.cpMin := cr.cpMax;
|
||||
cr.cpMax := cr.cpMax + s.text.Length;
|
||||
SendMessage(memoMessage.Handle, EM_EXSETSEL, 0, NativeUint(@cr));
|
||||
cf.crTextColor := ColorToRGB(s.color);
|
||||
if s.underline then cf.dwEffects := CFE_UNDERLINE else cf.dwEffects := 0;
|
||||
SendMessage(memoMessage.Handle, EM_SETCHARFORMAT, SCF_SELECTION, NativeUint(@cf));
|
||||
end;
|
||||
cr.cpMin := cr.cpMax;
|
||||
|
|
@ -251,15 +281,45 @@ begin
|
|||
begin
|
||||
AddText(':', Color_Text);
|
||||
AddText(IntToStr(Line), Color_Line);
|
||||
end
|
||||
else
|
||||
begin
|
||||
// Keep same number of segments
|
||||
AddText('', Color_Text);
|
||||
AddText('', Color_Text);
|
||||
end;
|
||||
AddText(' - ', Color_Text);
|
||||
AddText(ProjectLogStateTitle[state], FColor);
|
||||
AddText(' KM'+IntToHex(MsgCode, 5), Color_Code);
|
||||
|
||||
if MsgCode = 0 then
|
||||
begin
|
||||
AddText(ProjectLogStateTitle[state], FColor);
|
||||
AddText('', Color_Text);
|
||||
end
|
||||
else
|
||||
begin
|
||||
AddText(ProjectLogStateTitle[state]+' ', FColor);
|
||||
AddText('KM'+IntToHex(MsgCode, 5), Color_Code, True);
|
||||
end;
|
||||
|
||||
AddText(': ', Color_Text);
|
||||
AddText(StringReplace(msg, #13#10, ' ', [rfReplaceAll]), FTextColor);
|
||||
|
||||
AddText(#13#10, clBlack);
|
||||
|
||||
// Calculate segment lengths for later cursor + link calculations
|
||||
|
||||
x := 0;
|
||||
for i := 0 to High(Segments) - 1 do
|
||||
begin
|
||||
Segments[i].index := x; Inc(x, Segments[i].text.Length);
|
||||
end;
|
||||
mi.Segments := Segments;
|
||||
mi.LineLength := x;
|
||||
|
||||
AddLine;
|
||||
|
||||
// Strip #$D#$A from segments for further calculations
|
||||
SetLength(mi.Segments, Length(mi.Segments) - 1);
|
||||
finally
|
||||
SendMessage(memoMessage.Handle, WM_SETREDRAW, 1, 0);
|
||||
SendMessage(memoMessage.Handle, EM_SETEVENTMASK, 0, eventMask);
|
||||
|
|
@ -293,7 +353,7 @@ begin
|
|||
|
||||
SelLine := line;
|
||||
|
||||
mi := FMessageItems[line] as TMessageItem;
|
||||
mi := FMessageItems[line];
|
||||
FFilename := mi.FileName;
|
||||
|
||||
if FFileName = FGlobalProject.FileName then
|
||||
|
|
@ -333,6 +393,74 @@ begin
|
|||
Key := 0;
|
||||
end;
|
||||
|
||||
procedure TfrmMessages.memoMessageMouseDown(Sender: TObject;
|
||||
Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
|
||||
var
|
||||
pt: TPointL;
|
||||
ch: Integer;
|
||||
px: TPoint;
|
||||
begin
|
||||
if Button = mbRight then
|
||||
begin
|
||||
pt.x := X;
|
||||
pt.y := Y;
|
||||
ch := SendMessage(memoMessage.Handle, EM_CHARFROMPOS, 0, NativeInt(@pt));
|
||||
memoMessage.SelStart := ch;
|
||||
memoMessage.SelLength := 0;
|
||||
px := memoMessage.ClientToScreen(Point(X, Y));
|
||||
mnuPopup.Popup(px.X, px.Y);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TfrmMessages.memoMessageMouseMove(Sender: TObject; Shift: TShiftState;
|
||||
X, Y: Integer);
|
||||
var
|
||||
pt: TPOINTL;
|
||||
ch: Integer;
|
||||
Row, Col, i: Integer;
|
||||
mi: TMessageItem;
|
||||
seg: TMessageItemSegment;
|
||||
begin
|
||||
pt.x := X;
|
||||
pt.y := Y;
|
||||
ch := SendMessage(memoMessage.Handle, EM_CHARFROMPOS, 0, NativeInt(@pt));
|
||||
|
||||
x := 0;
|
||||
Row := -1;
|
||||
for I := 0 to FMessageItems.Count - 1 do
|
||||
begin
|
||||
if x + FMessageItems[i].LineLength >= ch then
|
||||
begin
|
||||
Row := i;
|
||||
Break;
|
||||
end;
|
||||
Inc(x, FMessageItems[i].LineLength + 1);
|
||||
end;
|
||||
|
||||
if Row < 0 then
|
||||
begin
|
||||
// Mouse is outside bounds of text
|
||||
memoMessage.Cursor := crDefault;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
Col := ch - x;
|
||||
mi := FMessageItems[Row];
|
||||
|
||||
if Length(mi.Segments) <= Segment_Code then
|
||||
begin
|
||||
// Safety check
|
||||
memoMessage.Cursor := crDefault;
|
||||
Exit;
|
||||
end;
|
||||
|
||||
seg := mi.Segments[Segment_Code];
|
||||
|
||||
if (Col >= seg.index) and (Col < seg.index + seg.text.Length)
|
||||
then memoMessage.Cursor := crHandPoint
|
||||
else memoMessage.Cursor := crDefault;
|
||||
end;
|
||||
|
||||
procedure TfrmMessages.cmdmSaveToFileClick(Sender: TObject);
|
||||
begin
|
||||
if dlgSave.Execute then
|
||||
|
|
@ -433,16 +561,37 @@ begin
|
|||
memoMessage.SelLength := Length(memoMessage.Lines[Value]);
|
||||
end;
|
||||
|
||||
procedure TfrmMessages.mnuOpenDocumentationClick(Sender: TObject);
|
||||
var
|
||||
line: Integer;
|
||||
mi: TMessageItem;
|
||||
begin
|
||||
line := SelLine;
|
||||
if line < 0 then Exit;
|
||||
mi := FMessageItems[line];
|
||||
if Length(mi.Segments) <= Segment_Code then
|
||||
Exit;
|
||||
|
||||
TUtilExecute.URL(URL_KmcMessage(mi.Segments[Segment_Code].text));
|
||||
end;
|
||||
|
||||
procedure TfrmMessages.mnuPopupPopup(Sender: TObject);
|
||||
var
|
||||
e: Boolean;
|
||||
line: Integer;
|
||||
begin
|
||||
e := memoMessage.Lines.Count > 0;
|
||||
cmdmNextMessage.Enabled := e and (SelLine < memoMessage.Lines.Count - 1);
|
||||
cmdmPreviousMessage.Enabled := e and (SelLine > 0);
|
||||
if e
|
||||
then line := SelLine
|
||||
else line := -1;
|
||||
cmdmNextMessage.Enabled := e and (line < memoMessage.Lines.Count - 1);
|
||||
cmdmPreviousMessage.Enabled := e and (line > 0);
|
||||
e := e and (line >= 0) and (line < FMessageItems.Count);
|
||||
|
||||
mnuViewItem.Enabled := e;
|
||||
cmdmClear.Enabled := e;
|
||||
cmdmSaveToFile.Enabled := e;
|
||||
mnuOpenDocumentation.Enabled := e;
|
||||
cmdmClear.Enabled := FMessageItems.Count > 0;
|
||||
cmdmSaveToFile.Enabled := FMessageItems.Count > 0;
|
||||
end;
|
||||
|
||||
procedure TfrmMessages.mnuViewItemClick(Sender: TObject);
|
||||
|
|
@ -451,14 +600,30 @@ begin
|
|||
end;
|
||||
|
||||
procedure TfrmMessages.memoMessageClick(Sender: TObject);
|
||||
var
|
||||
p: TPoint;
|
||||
mi: TMessageItem;
|
||||
seg: TMessageItemSegment;
|
||||
begin
|
||||
;
|
||||
p := memoMessage.CaretPos;
|
||||
if (p.Y < 0) or (p.Y >= FMessageItems.Count) then
|
||||
Exit;
|
||||
|
||||
mi := FMessageItems[p.Y];
|
||||
if Length(mi.Segments) <= Segment_Code then
|
||||
Exit;
|
||||
|
||||
seg := mi.Segments[Segment_Code];
|
||||
if (p.X >= seg.index) and (p.X < seg.index + seg.text.Length) then
|
||||
begin
|
||||
TUtilExecute.URL(URL_KmcMessage(seg.text));
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TfrmMessages.FormCreate(Sender: TObject);
|
||||
begin
|
||||
inherited;
|
||||
FMessageItems := TObjectList.Create;
|
||||
FMessageItems := TMessageItemList.Create;
|
||||
end;
|
||||
|
||||
procedure TfrmMessages.FormDestroy(Sender: TObject);
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ end;
|
|||
procedure TfrmCloneLocalProjectParameters.EnableControls;
|
||||
var
|
||||
e: Boolean;
|
||||
sourceId, id: string;
|
||||
id: string;
|
||||
begin
|
||||
e := (Trim(editSourceProjectFilename.Text) <> '') and
|
||||
FileExists(Trim(editSourceProjectFilename.Text)) and
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue