diff --git a/common/windows/delphi/general/Upload_Settings.pas b/common/windows/delphi/general/Upload_Settings.pas index bf2051b556..e1e8ce4294 100644 --- a/common/windows/delphi/general/Upload_Settings.pas +++ b/common/windows/delphi/general/Upload_Settings.pas @@ -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. diff --git a/common/windows/delphi/general/VersionInfo.pas b/common/windows/delphi/general/VersionInfo.pas index 7567279196..3e399f2f0c 100644 --- a/common/windows/delphi/general/VersionInfo.pas +++ b/common/windows/delphi/general/VersionInfo.pas @@ -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; diff --git a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts index 8a16de6bf1..6c004d101c 100644 --- a/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts +++ b/developer/src/common/web/test-helpers/verifyCompilerMessagesObject.ts @@ -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, 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); diff --git a/developer/src/common/web/utils/src/compiler-interfaces.ts b/developer/src/common/web/utils/src/compiler-interfaces.ts index 2f72982bf8..869e0165da 100644 --- a/developer/src/common/web/utils/src/compiler-interfaces.ts +++ b/developer/src/common/web/utils/src/compiler-interfaces.ts @@ -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 ?? ``); export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({ diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index f8ee6246da..f3e23e79f2 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -52,7 +52,7 @@ export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerE ALL_COMPILER_LOG_FORMATS, CompilerLogFormat, CompilerMessageOverride, CompilerMessageOverrideMap, - + dedentCompilerMessageDetail, KeymanCompilerArtifact, KeymanCompilerArtifactOptional, KeymanCompilerArtifacts, diff --git a/developer/src/kmc-copy/src/copier-messages.ts b/developer/src/kmc-copy/src/copier-messages.ts index c74ab25469..d4751474b9 100644 --- a/developer/src/kmc-copy/src/copier-messages.ts +++ b/developer/src/kmc-copy/src/copier-messages.ts @@ -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( diff --git a/developer/src/kmc-generate/src/generator-messages.ts b/developer/src/kmc-generate/src/generator-messages.ts index 6c9ccfa7d8..185f68cbe6 100644 --- a/developer/src/kmc-generate/src/generator-messages.ts +++ b/developer/src/kmc-generate/src/generator-messages.ts @@ -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; diff --git a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts index 8d8b8ca6ed..e58b5fb9bf 100644 --- a/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts +++ b/developer/src/kmc-kmn/src/kmw-compiler/kmw-compiler-messages.ts @@ -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. + `); + + //------------------------------------------------------------------------------| }; diff --git a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts index 5df0da4b73..3577a2e110 100644 --- a/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts +++ b/developer/src/kmc-ldml/src/compiler/ldml-compiler-messages.ts @@ -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 ``.' - ); + `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 + \`\`. + `); } diff --git a/developer/src/kmc/src/commands/messageCommand.ts b/developer/src/kmc/src/commands/messageCommand.ts index 257eacc913..d0feac380e 100644 --- a/developer/src/kmc/src/commands/messageCommand.ts +++ b/developer/src/kmc/src/commands/messageCommand.ts @@ -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' diff --git a/developer/src/tike/main/UfrmMessages.dfm b/developer/src/tike/main/UfrmMessages.dfm index bed2632463..aed03fb40c 100644 --- a/developer/src/tike/main/UfrmMessages.dfm +++ b/developer/src/tike/main/UfrmMessages.dfm @@ -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 diff --git a/developer/src/tike/main/UfrmMessages.pas b/developer/src/tike/main/UfrmMessages.pas index f57e097fc4..9a76254c96 100644 --- a/developer/src/tike/main/UfrmMessages.pas +++ b/developer/src/tike/main/UfrmMessages.pas @@ -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; + LineLength: Integer; end; + TMessageItemList = class(TObjectList); + 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; + 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); diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas index dc75f96d1f..cfc24801b3 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.UfrmCloneLocalProjectParameters.pas @@ -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