spiegel-keyman/developer/src/kmconvert/Keyman.Developer.System.ModelProjectTemplate.pas
Marc Durdin 39b7c882a3 feat(developer): Multi-process model for projects 🦕
This is a bit of an omnibus commit, apologies for that. This commit
moves Keyman Developer to a proper multi-process model, where editing
files from multiple projects is handled much more cleanly, with each
project loaded in a separate process.

Files that are edited outside of a project structure are loaded into a
'temporary project' in a single process, which provides a pathway for
existing users who may have legacy files outside the normal Keyman
Developer project model.

There are several components to this:

1. Inter-process communication (Multiprocess, CopyDataHelper units and
   multiprocess test project). These modules establish a method of
   enumerating running Keyman Developer instances (with EnumWindow),
   recording relevant metadata for each instance (by thread id) in the
   registry (registry used because it manages contention without
   additional effort from us), and communicating between processes with
   WM_COPYDATA.
2. Command-line parsing (TikeCommandLine). Determines project ownership
   (ProjectOwningFile) for each filename passed on the command-line, and
   passes these over to existing instances of Keyman Developer that have
   that project loaded, or starts new instances as needed.
3. Temporary project management. Mostly in ProjectUI.

Also, moved GlobalProjectStateWnd management out of ProjectFile.pas and
into Project.pas, alongside other global project variables.

A follow-up will add functionality to determine if a file opened within
the Keyman Developer UI should open in the same instance or in a
separate instance (see TfrmKeymanDeveloper.OpenFileInProject). This will
use the same methodology as TikeCommandLine does now, so may involve
further refactoring.

This commit establishes the idea of 'keyman.kpj' as a future default
filename for Keyman Developer projects, but does not enable it, in
ProjectOwningFile. It is planned to introduce this fixed filename in
version 18.0 (see #10113).
2023-12-01 11:48:09 +07:00

208 lines
5.5 KiB
ObjectPascal

unit Keyman.Developer.System.ModelProjectTemplate;
interface
uses
System.SysUtils,
Keyman.Developer.System.ProjectTemplate,
kpsfile,
UKeymanTargets,
utilfiletypes;
type
EModelProjectTemplate = class(Exception);
TModelProjectTemplate = class(TProjectTemplate)
private
procedure WriteModel;
procedure WriteWordlist;
procedure WriteKPS;
procedure WriteKPJ;
procedure WriteModelInfo;
function GetModelFilename: string;
function GetWordlistFilename: string;
protected
const
SFileTemplate_ModelInfo = '%s.model_info'; // in root
SFileTemplate_Package = '%s.model.kps';
SFilename_Model = 'model.ts'; // Only for source
SFilename_Wordlist = 'wordlist.tsv';
SFileTemplate_Model = '%s.model.ts';
SFileTemplate_CompiledModel = '%s.model.js';
SDataPath_WordlistLexicalModel = 'wordlist-lexical-model\';
function DataPath: string; override;
function GetPackageFilename: string; override;
public
constructor Create(const BasePath, ModelID: string);
procedure Generate; override;
property ModelFilename: string read GetModelFilename;
property WordlistFilename: string read GetWordlistFilename;
end;
implementation
uses
System.Classes,
System.StrUtils,
BCP47Tag,
Keyman.System.LanguageCodeUtils,
KeymanVersion,
Keyman.Developer.System.Project.modelTsProjectFile,
Keyman.Developer.System.Project.kpsProjectFile,
Keyman.Developer.System.Project.wordlistTsvProjectFile,
Keyman.Developer.System.Project.ProjectFile,
Keyman.Developer.System.Project.ProjectFiles,
PackageInfo,
RedistFiles,
utilstr;
{ TModelProjectTemplate }
constructor TModelProjectTemplate.Create(const BasePath, ModelID: string);
begin
inherited Create(BasePath, ModelID, TouchKeymanTargets);
end;
procedure TModelProjectTemplate.Generate;
begin
if not ForceDirectories(BasePath + ID + '\' + SFolder_Source) then
raise EModelProjectTemplate.Create('Could not create destination path '+BasePath+ID);
WriteDocumentation;
WriteModel;
WriteWordlist;
WriteKPS;
WriteKPJ;
WriteRepositoryMetadata;
WriteModelInfo;
end;
function TModelProjectTemplate.GetModelFilename: string;
begin
Result := GetFilename(Format(SFileTemplate_Model, [ID]));
end;
function TModelProjectTemplate.GetWordlistFilename: string;
begin
Result := GetFilename(SFilename_Wordlist);
end;
function TModelProjectTemplate.GetPackageFilename: string;
begin
Result := GetFilename(Format(SFileTemplate_Package, [ID]));
end;
procedure TModelProjectTemplate.WriteModel;
begin
Transform(SFolder_Source + '\' + SFilename_Model,
Format(SFolder_Source + '\' + SFileTemplate_Model, [ID]));
end;
procedure TModelProjectTemplate.WriteModelInfo;
begin
// Write modelid.model_info
Transform(
Format(SFileTemplate_ModelInfo, ['model']),
Format(SFileTemplate_ModelInfo, [ID])
);
end;
procedure TModelProjectTemplate.WriteWordlist;
begin
Transform(SFolder_Source + '\' + SFilename_Wordlist);
end;
procedure TModelProjectTemplate.WriteKPJ;
var
kpj: TProject;
begin
kpj := TProject.Create(ptLexicalModel, GetProjectFilename, False);
try
kpj.Options.BuildPath := '$PROJECTPATH\' + SFolder_Build;
kpj.Options.WarnDeprecatedCode := True;
kpj.Options.CompilerWarningsAsErrors := True;
kpj.Options.CheckFilenameConventions := True;
// Add model and package to project
kpj.Files.Add(TmodelTsProjectFile.Create(kpj, GetModelFilename, nil));
kpj.Files.Add(TkpsProjectFile.Create(kpj, GetPackageFilename, nil));
// Add metadata files to project
kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_HistoryMD, nil));
kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_LicenseMD, nil));
kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + SFile_ReadmeMD, nil));
kpj.Files.Add(TOpenableProjectFile.Create(kpj, BasePath + ID + '\' + Format(SFileTemplate_ModelInfo, [ID]), nil));
kpj.Save;
finally
kpj.Free;
end;
end;
procedure TModelProjectTemplate.WriteKPS;
var
kps: TKPSFile;
f: TPackageContentFile;
plm: TPackageLexicalModel;
begin
kps := TKPSFile.Create;
try
// Set kps metadata
kps.Info.Desc[PackageInfo_Name] := Name;
kps.Info.Desc[PackageInfo_Copyright] := Copyright;
kps.Info.Desc[PackageInfo_Author] := Author;
kps.Info.Desc[PackageInfo_Version] := Version;
kps.Info.Desc[PackageInfo_Description] := Description;
kps.FileName := GetPackageFilename;
// Add model.js
f := TPackageContentFile.Create(kps);
f.FileName := BasePath + ID + '\' + SFolder_Build + '\' + Format(SFileTemplate_CompiledModel, [ID]);
kps.Files.Add(f);
// Add welcome
f := TPackageContentFile.Create(kps);
f.FileName := BasePath + ID + '\' + SFolder_Source + '\' + SFile_WelcomeHTM;
kps.Files.Add(f);
// Add readme
f := TPackageContentFile.Create(kps);
f.FileName := BasePath + ID + '\' + SFolder_Source + '\' + SFile_ReadmeHTM;
kps.Files.Add(f);
kps.Options.ReadmeFile := f;
// Add license
f := TPackageContentFile.Create(kps);
f.FileName := BasePath + ID + '\' + SFile_LicenseMD;
kps.Files.Add(f);
kps.Options.LicenseFile := f;
// Add metadata about the lexical model
plm := TPackageLexicalModel.Create(kps);
plm.Name := Name;
plm.ID := ID;
kps.LexicalModels.Add(plm);
SetPackageLanguageMetadata(kps, plm.Languages);
kps.SaveXML;
finally
kps.Free;
end;
end;
function TModelProjectTemplate.DataPath: string;
begin
Result := inherited DataPath + SDataPath_WordlistLexicalModel;
end;
end.