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:
Marc Durdin 2025-02-10 08:19:11 +07:00 committed by GitHub
commit 2d5e3ee440
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 373 additions and 106 deletions

View file

@ -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.

View file

@ -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;

View file

@ -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);

View file

@ -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 => ({

View file

@ -52,7 +52,7 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerE
ALL_COMPILER_LOG_FORMATS, CompilerLogFormat,
CompilerMessageOverride,
CompilerMessageOverrideMap,
dedentCompilerMessageDetail,
KeymanCompilerArtifact,
KeymanCompilerArtifactOptional,
KeymanCompilerArtifacts,

View file

@ -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(

View file

@ -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;

View file

@ -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.
`);
//------------------------------------------------------------------------------|
};

View file

@ -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]"/>\`.
`);
}

View file

@ -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'

View file

@ -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

View file

@ -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);

View file

@ -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