View Issue Details

IDProjectCategoryView StatusLast Update
0022752LazarusIDEpublic2019-04-14 09:37
ReportercarliAssigned ToJuha Manninen 
PrioritynormalSeverityminorReproducibilityalways
Status feedbackResolutionreopened 
Product VersionProduct Build 
Target VersionFixed in Version 
Summary0022752: Unit list mess in .lpi file
DescriptionLazarus' representation of the unit list in XML is to have
<Units Count="83">
 <Unit0/>
 <Unit1/>
 <Unit2/>
</Units>

This is not optimal.
 - When two project members add a file to the project, the merge conflicts due to number clashes
 - When files get deleted from the project, all numbers get shifted; merging is impossible. I have to resolve the conflict manually. Commits contain lot of number shifting crap.

So my request is:
 - Remove the field "count"
 - Remove the unit number in the XML tag
 - (implement the reader such that it reads old-fashion-lpi files, too)
TagsNo tags attached.
Fixed in Revisionr60683
LazTarget-
Widgetset
Attached Files
  • xmlcfg-nodeindexes-01.patch (9,393 bytes)
    Index: components/lazutils/laz2_xmlcfg.pas
    ===================================================================
    --- components/lazutils/laz2_xmlcfg.pas	(revision 60489)
    +++ components/lazutils/laz2_xmlcfg.pas	(working copy)
    @@ -52,6 +52,7 @@
         type
           TNodeCache = record
             Node: TDomNode;
    +        NodeSearchName: string;
             ChildrenValid: boolean;
             Children: array of TDomNode; // nodes with NodeName<>'' and sorted
           end;
    @@ -68,13 +69,15 @@
         procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); virtual;
         procedure WriteXMLFile(ADoc: TXMLDocument; const AFileName: String); virtual;
         procedure FreeDoc; virtual;
    -    procedure SetPathNodeCache(Index: integer; aNode: TDomNode);
    +    procedure SetPathNodeCache(Index: integer; aNode: TDomNode; aNodeSearchName: string = '');
         function GetCachedPathNode(Index: integer): TDomNode; inline;
    +    function GetCachedPathNode(Index: integer; out aNodeSearchName: string): TDomNode; inline;
         procedure InvalidateCacheTilEnd(StartIndex: integer);
         function InternalFindNode(const APath: String; PathLen: integer;
                                   CreateNodes: boolean = false): TDomNode;
         procedure InternalCleanNode(Node: TDomNode);
    -    function FindChildNode(PathIndex: integer; const aName: string): TDomNode;
    +    function FindChildNode(PathIndex: integer; const aName: string;
    +      CreateNodes: boolean = false): TDomNode;
       public
         constructor Create(AOwner: TComponent); override; overload;
         constructor Create(const AFilename: String); overload; // create and load
    @@ -109,6 +112,7 @@
         // checks if the path has values, set PathHasValue=true to skip the last part
         function HasPath(const APath: string; PathHasValue: boolean): boolean;
         function HasChildPaths(const APath: string): boolean;
    +    function GetChildCount(const APath: string): Integer;
         property Modified: Boolean read FModified write FModified;
         procedure InvalidatePathCache;
       published
    @@ -151,14 +155,31 @@
     end;
     
     // inline
    -function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
    +function TXMLConfig.GetCachedPathNode(Index: integer; out
    +  aNodeSearchName: string): TDomNode;
     begin
       if Index<length(fPathNodeCache) then
    -    Result:=fPathNodeCache[Index].Node
    -  else
    +  begin
    +    Result:=fPathNodeCache[Index].Node;
    +    aNodeSearchName:=fPathNodeCache[Index].NodeSearchName;
    +  end else
    +  begin
         Result:=nil;
    +    aNodeSearchName:='';
    +  end;
     end;
     
    +function TXMLConfig.GetChildCount(const APath: string): Integer;
    +var
    +  Node: TDOMNode;
    +begin
    +  Node:=FindNode(APath,false);
    +  if Node=nil then
    +    Result := 0
    +  else
    +    Result := Node.GetChildCount;
    +end;
    +
     constructor TXMLConfig.Create(const AFilename: String);
     begin
       //DebugLn(['TXMLConfig.Create ',AFilename]);
    @@ -478,8 +499,16 @@
       FreeAndNil(doc);
     end;
     
    -procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode);
    +function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
     var
    +  x: string;
    +begin
    +  Result := GetCachedPathNode(Index, x);
    +end;
    +
    +procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode;
    +  aNodeSearchName: string);
    +var
       OldLength, NewLength: Integer;
     begin
       OldLength:=length(fPathNodeCache);
    @@ -495,8 +524,11 @@
         exit
       else
         InvalidateCacheTilEnd(Index+1);
    +  if aNodeSearchName='' then
    +    aNodeSearchName:=aNode.NodeName;
       with fPathNodeCache[Index] do begin
         Node:=aNode;
    +    NodeSearchName:=aNodeSearchName;
         ChildrenValid:=false;
       end;
     end;
    @@ -517,11 +549,9 @@
     function TXMLConfig.InternalFindNode(const APath: String; PathLen: integer;
       CreateNodes: boolean): TDomNode;
     var
    -  NodePath: String;
    +  NodePath, NdName: String;
       StartPos, EndPos: integer;
       PathIndex: Integer;
    -  Parent: TDOMNode;
    -  NdName: DOMString;
       NameLen: Integer;
     begin
       //debugln(['TXMLConfig.InternalFindNode APath="',copy(APath,1,PathLen),'" CreateNodes=',CreateNodes]);
    @@ -539,25 +569,15 @@
         NameLen:=EndPos-StartPos;
         if NameLen=0 then break;
         inc(PathIndex);
    -    Parent:=Result;
    -    Result:=GetCachedPathNode(PathIndex);
    -    if Result<>nil then
    -      NdName:=Result.NodeName;
    +    Result:=GetCachedPathNode(PathIndex,NdName);
         if (Result=nil) or (length(NdName)<>NameLen)
         or not CompareMem(PChar(NdName),@APath[StartPos],NameLen) then begin
           // different path => search
           NodePath:=copy(APath,StartPos,NameLen);
    -      Result:=FindChildNode(PathIndex-1,NodePath);
    -      if Result=nil then begin
    -        if not CreateNodes then exit;
    -        // create missing node
    -        Result:=Doc.CreateElement(NodePath);
    -        Parent.AppendChild(Result);
    -        fPathNodeCache[PathIndex-1].ChildrenValid:=false;
    -        InvalidateCacheTilEnd(PathIndex);
    -        if EndPos>PathLen then exit;
    -      end;
    -      SetPathNodeCache(PathIndex,Result);
    +      Result:=FindChildNode(PathIndex-1,NodePath,CreateNodes);
    +      if Result=nil then
    +        Exit;
    +      SetPathNodeCache(PathIndex,Result,NodePath);
         end;
         StartPos:=EndPos+1;
         if StartPos>PathLen then exit;
    @@ -581,8 +601,9 @@
       end;
     end;
     
    -function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string
    -  ): TDomNode;
    +function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string;
    +  CreateNodes: boolean): TDomNode;
    +
     var
       aParent, aChild: TDOMNode;
       aCount: Integer;
    @@ -589,6 +610,8 @@
       NewLength: Integer;
       l, r, m: Integer;
       cmp: Integer;
    +  BrPos: SizeInt;
    +  NewName: string;
     begin
       with fPathNodeCache[PathIndex] do begin
         if not ChildrenValid then begin
    @@ -623,20 +646,53 @@
           ChildrenValid:=true;
         end;
     
    -    // binary search
    -    l:=0;
    -    r:=length(Children)-1;
    -    while l<=r do begin
    -      m:=(l+r) shr 1;
    -      cmp:=CompareStr(aName,Children[m].NodeName);
    -      if cmp<0 then
    -        r:=m-1
    -      else if cmp>0 then
    -        l:=m+1
    -      else
    -        exit(Children[m]);
    +    BrPos := Pos('[', aName);
    +    if (Length(aName)>=BrPos+2) and (aName[Length(aName)]=']')
    +    and TryStrToInt(Trim(Copy(aName, BrPos+1, Length(aName)-BrPos-1)), m) then
    +    begin
    +      // support XPath in format "*[?]" or "name[?]" - !!! the name is actually not checked - only the index
    +      // the name is used only for creating child nodes
    +      // do not use Children here because they are sorted and filtered
    +      if m<=0 then
    +        Result:=nil // error
    +      else if (m<=Node.ChildNodes.Count) then
    +        Result:=Node.ChildNodes[m-1]
    +      else if CreateNodes then
    +      begin
    +        NewName := Trim(Copy(aName, 1, BrPos-1));
    +        for m := Node.ChildNodes.Count+1 to m do
    +        begin
    +          Result:=Doc.CreateElement(NewName);
    +          Node.AppendChild(Result);
    +        end;
    +        fPathNodeCache[PathIndex].ChildrenValid:=false;
    +        InvalidateCacheTilEnd(PathIndex+1);
    +      end;
    +    end else
    +    begin
    +      // binary search
    +      l:=0;
    +      r:=length(Children)-1;
    +      while l<=r do begin
    +        m:=(l+r) shr 1;
    +        cmp:=CompareStr(aName,Children[m].NodeName);
    +        if cmp<0 then
    +          r:=m-1
    +        else if cmp>0 then
    +          l:=m+1
    +        else
    +          exit(Children[m]);
    +      end;
    +      if CreateNodes then
    +      begin
    +        // create missing node
    +        Result:=Doc.CreateElement(aName);
    +        Node.AppendChild(Result);
    +        fPathNodeCache[PathIndex].ChildrenValid:=false;
    +        InvalidateCacheTilEnd(PathIndex+1);
    +      end else
    +        Result:=nil;
         end;
    -    Result:=nil;
       end;
     end;
     
    Index: ide/project.pp
    ===================================================================
    --- ide/project.pp	(revision 60489)
    +++ ide/project.pp	(working copy)
    @@ -1134,7 +1134,7 @@
     implementation
     
     const
    -  ProjectInfoFileVersion = 11;
    +  ProjectInfoFileVersion = 12;
       ProjOptionsPath = 'ProjectOptions/';
     
     
    @@ -2816,9 +2816,9 @@
       MergeUnitInfo: Boolean;
     begin
       {$IFDEF IDE_MEM_CHECK}CheckHeapWrtMemCnt('TProject.ReadProject D reading units');{$ENDIF}
    -  NewUnitCount:=FXMLConfig.GetValue(Path+'Units/Count',0);
    +  NewUnitCount:=FXMLConfig.GetChildCount(Path+'Units');
       for i := 0 to NewUnitCount - 1 do begin
    -    SubPath:=Path+'Units/Unit'+IntToStr(i)+'/';
    +    SubPath:=Path+'Units/Unit['+IntToStr(i+1)+']/';
         NewUnitFilename:=FXMLConfig.GetValue(SubPath+'Filename/Value','');
         OnLoadSaveFilename(NewUnitFilename,true);
         // load unit and add it
    @@ -2867,7 +2867,7 @@
     const
       Path = ProjOptionsPath;
     begin
    -  if (FFileVersion=0) and (FXMLConfig.GetValue(Path+'Units/Count',0)=0) then
    +  if (FFileVersion=0) and (FXMLConfig.GetChildCount(Path+'Units')=0) then
         if IDEMessageDialog(lisStrangeLpiFile,
             Format(lisTheFileDoesNotLookLikeALpiFile, [ProjectInfoFile]),
             mtConfirmation,[mbIgnore,mbAbort])<>mrIgnore
    @@ -3149,10 +3149,9 @@
       for i:=0 to UnitCount-1 do
         if UnitMustBeSaved(Units[i],FProjectWriteFlags,SaveSession) then begin
           Units[i].SaveToXMLConfig(FXMLConfig,
    -        Path+'Units/Unit'+IntToStr(SaveUnitCount)+'/',True,SaveSession,fCurStorePathDelim);
    +        Path+'Units/Unit['+IntToStr(SaveUnitCount+1)+']/',True,SaveSession,fCurStorePathDelim);
           inc(SaveUnitCount);
         end;
    -  FXMLConfig.SetDeleteValue(Path+'Units/Count',SaveUnitCount,0);
     end;
     
     procedure TProject.SaveOtherDefines(const Path: string);
    
  • xmlcfg-nodeindexes-02.patch (18,289 bytes)
    Index: components/ideintf/projectintf.pas
    ===================================================================
    --- components/ideintf/projectintf.pas	(revision 60489)
    +++ components/ideintf/projectintf.pas	(working copy)
    @@ -247,7 +247,8 @@
         pfLRSFilesInOutputDirectory, // put .lrs files in output directory
         pfUseDefaultCompilerOptions, // load users default compiler options
         pfSaveJumpHistory,
    -    pfSaveFoldState
    +    pfSaveFoldState,
    +    pfCompatibilityMode // use legacy file format to maximize compatibility with old Lazarus versions
         );
       TProjectFlags = set of TProjectFlag;
     
    @@ -274,7 +275,8 @@
           'LRSInOutputDirectory',
           'UseDefaultCompilerOptions',
           'SaveJumpHistory',
    -      'SaveFoldState'
    +      'SaveFoldState',
    +      'CompatibilityMode'
         );
       ProjectSessionStorageNames: array[TProjectSessionStorage] of string = (
         'InProjectInfo',
    Index: components/lazutils/laz2_xmlcfg.pas
    ===================================================================
    --- components/lazutils/laz2_xmlcfg.pas	(revision 60489)
    +++ components/lazutils/laz2_xmlcfg.pas	(working copy)
    @@ -52,6 +52,7 @@
         type
           TNodeCache = record
             Node: TDomNode;
    +        NodeSearchName: string;
             ChildrenValid: boolean;
             Children: array of TDomNode; // nodes with NodeName<>'' and sorted
           end;
    @@ -68,13 +69,15 @@
         procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); virtual;
         procedure WriteXMLFile(ADoc: TXMLDocument; const AFileName: String); virtual;
         procedure FreeDoc; virtual;
    -    procedure SetPathNodeCache(Index: integer; aNode: TDomNode);
    +    procedure SetPathNodeCache(Index: integer; aNode: TDomNode; aNodeSearchName: string = '');
         function GetCachedPathNode(Index: integer): TDomNode; inline;
    +    function GetCachedPathNode(Index: integer; out aNodeSearchName: string): TDomNode; inline;
         procedure InvalidateCacheTilEnd(StartIndex: integer);
         function InternalFindNode(const APath: String; PathLen: integer;
                                   CreateNodes: boolean = false): TDomNode;
         procedure InternalCleanNode(Node: TDomNode);
    -    function FindChildNode(PathIndex: integer; const aName: string): TDomNode;
    +    function FindChildNode(PathIndex: integer; const aName: string;
    +      CreateNodes: boolean = false): TDomNode;
       public
         constructor Create(AOwner: TComponent); override; overload;
         constructor Create(const AFilename: String); overload; // create and load
    @@ -109,6 +112,10 @@
         // checks if the path has values, set PathHasValue=true to skip the last part
         function HasPath(const APath: string; PathHasValue: boolean): boolean;
         function HasChildPaths(const APath: string): boolean;
    +    function GetChildCount(const APath: string): Integer;
    +    function GetListItemCount(const APath: string; const ALegacyList: Boolean): Integer;
    +    function GetListItemXPath(const AName: string; const AIndex: Integer; const ALegacyList: Boolean): string;
    +    procedure SetListItemCount(const APath: string; const ACount: Integer; const ALegacyList: Boolean);
         property Modified: Boolean read FModified write FModified;
         procedure InvalidatePathCache;
       published
    @@ -151,14 +158,31 @@
     end;
     
     // inline
    -function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
    +function TXMLConfig.GetCachedPathNode(Index: integer; out
    +  aNodeSearchName: string): TDomNode;
     begin
       if Index<length(fPathNodeCache) then
    -    Result:=fPathNodeCache[Index].Node
    -  else
    +  begin
    +    Result:=fPathNodeCache[Index].Node;
    +    aNodeSearchName:=fPathNodeCache[Index].NodeSearchName;
    +  end else
    +  begin
         Result:=nil;
    +    aNodeSearchName:='';
    +  end;
     end;
     
    +function TXMLConfig.GetChildCount(const APath: string): Integer;
    +var
    +  Node: TDOMNode;
    +begin
    +  Node:=FindNode(APath,false);
    +  if Node=nil then
    +    Result := 0
    +  else
    +    Result := Node.GetChildCount;
    +end;
    +
     constructor TXMLConfig.Create(const AFilename: String);
     begin
       //DebugLn(['TXMLConfig.Create ',AFilename]);
    @@ -294,6 +318,24 @@
       Result:=StrToExtended(GetValue(APath,''),ADefault);
     end;
     
    +function TXMLConfig.GetListItemCount(const APath: string;
    +  const ALegacyList: Boolean): Integer;
    +begin
    +  if ALegacyList then
    +    Result := GetValue(APath+'Count',0)
    +  else
    +    Result := GetChildCount(APath);
    +end;
    +
    +function TXMLConfig.GetListItemXPath(const AName: string;
    +  const AIndex: Integer; const ALegacyList: Boolean): string;
    +begin
    +  if ALegacyList then
    +    Result := AName+IntToStr(AIndex)
    +  else
    +    Result := AName+'['+IntToStr(AIndex+1)+']';
    +end;
    +
     procedure TXMLConfig.SetValue(const APath, AValue: String);
     var
       Node: TDOMNode;
    @@ -478,8 +520,16 @@
       FreeAndNil(doc);
     end;
     
    -procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode);
    +function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
     var
    +  x: string;
    +begin
    +  Result := GetCachedPathNode(Index, x);
    +end;
    +
    +procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode;
    +  aNodeSearchName: string);
    +var
       OldLength, NewLength: Integer;
     begin
       OldLength:=length(fPathNodeCache);
    @@ -495,8 +545,11 @@
         exit
       else
         InvalidateCacheTilEnd(Index+1);
    +  if aNodeSearchName='' then
    +    aNodeSearchName:=aNode.NodeName;
       with fPathNodeCache[Index] do begin
         Node:=aNode;
    +    NodeSearchName:=aNodeSearchName;
         ChildrenValid:=false;
       end;
     end;
    @@ -517,11 +570,9 @@
     function TXMLConfig.InternalFindNode(const APath: String; PathLen: integer;
       CreateNodes: boolean): TDomNode;
     var
    -  NodePath: String;
    +  NodePath, NdName: String;
       StartPos, EndPos: integer;
       PathIndex: Integer;
    -  Parent: TDOMNode;
    -  NdName: DOMString;
       NameLen: Integer;
     begin
       //debugln(['TXMLConfig.InternalFindNode APath="',copy(APath,1,PathLen),'" CreateNodes=',CreateNodes]);
    @@ -539,25 +590,15 @@
         NameLen:=EndPos-StartPos;
         if NameLen=0 then break;
         inc(PathIndex);
    -    Parent:=Result;
    -    Result:=GetCachedPathNode(PathIndex);
    -    if Result<>nil then
    -      NdName:=Result.NodeName;
    +    Result:=GetCachedPathNode(PathIndex,NdName);
         if (Result=nil) or (length(NdName)<>NameLen)
         or not CompareMem(PChar(NdName),@APath[StartPos],NameLen) then begin
           // different path => search
           NodePath:=copy(APath,StartPos,NameLen);
    -      Result:=FindChildNode(PathIndex-1,NodePath);
    -      if Result=nil then begin
    -        if not CreateNodes then exit;
    -        // create missing node
    -        Result:=Doc.CreateElement(NodePath);
    -        Parent.AppendChild(Result);
    -        fPathNodeCache[PathIndex-1].ChildrenValid:=false;
    -        InvalidateCacheTilEnd(PathIndex);
    -        if EndPos>PathLen then exit;
    -      end;
    -      SetPathNodeCache(PathIndex,Result);
    +      Result:=FindChildNode(PathIndex-1,NodePath,CreateNodes);
    +      if Result=nil then
    +        Exit;
    +      SetPathNodeCache(PathIndex,Result,NodePath);
         end;
         StartPos:=EndPos+1;
         if StartPos>PathLen then exit;
    @@ -581,8 +622,9 @@
       end;
     end;
     
    -function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string
    -  ): TDomNode;
    +function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string;
    +  CreateNodes: boolean): TDomNode;
    +
     var
       aParent, aChild: TDOMNode;
       aCount: Integer;
    @@ -589,6 +631,8 @@
       NewLength: Integer;
       l, r, m: Integer;
       cmp: Integer;
    +  BrPos: SizeInt;
    +  NewName: string;
     begin
       with fPathNodeCache[PathIndex] do begin
         if not ChildrenValid then begin
    @@ -623,20 +667,53 @@
           ChildrenValid:=true;
         end;
     
    -    // binary search
    -    l:=0;
    -    r:=length(Children)-1;
    -    while l<=r do begin
    -      m:=(l+r) shr 1;
    -      cmp:=CompareStr(aName,Children[m].NodeName);
    -      if cmp<0 then
    -        r:=m-1
    -      else if cmp>0 then
    -        l:=m+1
    -      else
    -        exit(Children[m]);
    +    BrPos := Pos('[', aName);
    +    if (Length(aName)>=BrPos+2) and (aName[Length(aName)]=']')
    +    and TryStrToInt(Trim(Copy(aName, BrPos+1, Length(aName)-BrPos-1)), m) then
    +    begin
    +      // support XPath in format "*[?]" or "name[?]" - !!! the name is actually not checked - only the index
    +      // the name is used only for creating child nodes
    +      // do not use Children here because they are sorted and filtered
    +      if m<=0 then
    +        Result:=nil // error
    +      else if (m<=Node.ChildNodes.Count) then
    +        Result:=Node.ChildNodes[m-1]
    +      else if CreateNodes then
    +      begin
    +        NewName := Trim(Copy(aName, 1, BrPos-1));
    +        for m := Node.ChildNodes.Count+1 to m do
    +        begin
    +          Result:=Doc.CreateElement(NewName);
    +          Node.AppendChild(Result);
    +        end;
    +        fPathNodeCache[PathIndex].ChildrenValid:=false;
    +        InvalidateCacheTilEnd(PathIndex+1);
    +      end;
    +    end else
    +    begin
    +      // binary search
    +      l:=0;
    +      r:=length(Children)-1;
    +      while l<=r do begin
    +        m:=(l+r) shr 1;
    +        cmp:=CompareStr(aName,Children[m].NodeName);
    +        if cmp<0 then
    +          r:=m-1
    +        else if cmp>0 then
    +          l:=m+1
    +        else
    +          exit(Children[m]);
    +      end;
    +      if CreateNodes then
    +      begin
    +        // create missing node
    +        Result:=Doc.CreateElement(aName);
    +        Node.AppendChild(Result);
    +        fPathNodeCache[PathIndex].ChildrenValid:=false;
    +        InvalidateCacheTilEnd(PathIndex+1);
    +      end else
    +        Result:=nil;
         end;
    -    Result:=nil;
       end;
     end;
     
    @@ -686,6 +763,13 @@
       {$IFDEF MEM_CHECK}CheckHeapWrtMemCnt('TXMLConfig.SetFilename END');{$ENDIF}
     end;
     
    +procedure TXMLConfig.SetListItemCount(const APath: string;
    +  const ACount: Integer; const ALegacyList: Boolean);
    +begin
    +  if ALegacyList then
    +    SetDeleteValue(APath+'Count',ACount,0)
    +end;
    +
     procedure TXMLConfig.CreateConfigNode;
     var
       cfg: TDOMElement;
    Index: ide/frames/project_misc_options.lfm
    ===================================================================
    --- ide/frames/project_misc_options.lfm	(revision 60489)
    +++ ide/frames/project_misc_options.lfm	(working copy)
    @@ -118,13 +118,13 @@
       end
       object ResourceGroupBox: TGroupBox
         AnchorSideLeft.Control = Owner
    -    AnchorSideTop.Control = LRSInOutputDirCheckBox
    +    AnchorSideTop.Control = CompatibilityModeCheckBox
         AnchorSideTop.Side = asrBottom
         AnchorSideRight.Control = Owner
         AnchorSideRight.Side = asrBottom
         Left = 0
         Height = 81
    -    Top = 231
    +    Top = 256
         Width = 536
         Anchors = [akTop, akLeft, akRight]
         BorderSpacing.Top = 6
    @@ -139,7 +139,7 @@
         ChildSizing.ControlsPerLine = 1
         ClientHeight = 61
         ClientWidth = 532
    -    TabOrder = 9
    +    TabOrder = 10
         object UseFPCResourcesRadioButton: TRadioButton
           Left = 6
           Height = 25
    @@ -183,7 +183,7 @@
         AnchorSideTop.Side = asrCenter
         Left = 0
         Height = 15
    -    Top = 331
    +    Top = 356
         Width = 83
         Caption = 'PathDelimLabel'
         ParentColor = False
    @@ -196,7 +196,7 @@
         AnchorSideRight.Side = asrBottom
         Left = 0
         Height = 3
    -    Top = 318
    +    Top = 343
         Width = 536
         Anchors = [akTop, akLeft, akRight]
         BorderSpacing.Top = 6
    @@ -209,12 +209,12 @@
         AnchorSideRight.Side = asrBottom
         Left = 89
         Height = 23
    -    Top = 327
    +    Top = 352
         Width = 259
         BorderSpacing.Left = 6
         BorderSpacing.Top = 6
         ItemHeight = 15
    -    TabOrder = 10
    +    TabOrder = 11
         Text = 'PathDelimComboBox'
       end
       object MainUnitHasScaledStatementCheckBox: TCheckBox
    @@ -231,4 +231,18 @@
         ShowHint = True
         TabOrder = 4
       end
    +  object CompatibilityModeCheckBox: TCheckBox
    +    AnchorSideLeft.Control = Owner
    +    AnchorSideTop.Control = LRSInOutputDirCheckBox
    +    AnchorSideTop.Side = asrBottom
    +    Left = 0
    +    Height = 19
    +    Top = 231
    +    Width = 175
    +    BorderSpacing.Top = 6
    +    Caption = 'CompatibilityModeCheckBox'
    +    ParentShowHint = False
    +    ShowHint = True
    +    TabOrder = 9
    +  end
     end
    Index: ide/frames/project_misc_options.pas
    ===================================================================
    --- ide/frames/project_misc_options.pas	(revision 60489)
    +++ ide/frames/project_misc_options.pas	(working copy)
    @@ -25,6 +25,7 @@
         Bevel2: TBevel;
         LRSInOutputDirCheckBox: TCheckBox;
         MainUnitHasCreateFormStatementsCheckBox: TCheckBox;
    +    CompatibilityModeCheckBox: TCheckBox;
         MainUnitHasTitleStatementCheckBox: TCheckBox;
         MainUnitHasScaledStatementCheckBox: TCheckBox;
         MainUnitHasUsesSectionForAllUnitsCheckBox: TCheckBox;
    @@ -68,6 +69,8 @@
       MainUnitHasTitleStatementCheckBox.Hint := lisIdeMaintainsTheTitleInMainUnit;
       MainUnitHasScaledStatementCheckBox.Caption := lisMainUnitHasApplicationScaledStatement;
       MainUnitHasScaledStatementCheckBox.Hint := lisIdeMaintainsScaledInMainUnit;
    +  CompatibilityModeCheckBox.Caption := lisLPICompatibilityModeCheckBox;
    +  CompatibilityModeCheckBox.Hint := lisLPICompatibilityModeCheckBoxHint;
       RunnableCheckBox.Caption := lisProjectIsRunnable;
       RunnableCheckBox.Hint := lisProjectIsRunnableHint;
       UseDesignTimePkgsCheckBox.Caption := lisUseDesignTimePackages;
    @@ -96,6 +99,7 @@
         MainUnitHasCreateFormStatementsCheckBox.Checked := (pfMainUnitHasCreateFormStatements in Flags);
         MainUnitHasTitleStatementCheckBox.Checked := (pfMainUnitHasTitleStatement in Flags);
         MainUnitHasScaledStatementCheckBox.Checked := (pfMainUnitHasScaledStatement in Flags);
    +    CompatibilityModeCheckBox.Checked := (pfCompatibilityMode in Flags);
         RunnableCheckBox.Checked := (pfRunnable in Flags);
         UseDesignTimePkgsCheckBox.Checked := (pfUseDesignTimePackages in Flags);
         AlwaysBuildCheckBox.Checked := (pfAlwaysBuild in Flags);
    @@ -140,6 +144,8 @@
                      MainUnitHasTitleStatementCheckBox.Checked);
       SetProjectFlag(pfMainUnitHasScaledStatement,
                      MainUnitHasScaledStatementCheckBox.Checked);
    +  SetProjectFlag(pfCompatibilityMode,
    +                 CompatibilityModeCheckBox.Checked);
       SetProjectFlag(pfRunnable, RunnableCheckBox.Checked);
       SetProjectFlag(pfUseDesignTimePackages, UseDesignTimePkgsCheckBox.Checked);
       SetProjectFlag(pfAlwaysBuild, AlwaysBuildCheckBox.Checked);
    Index: ide/lazarusidestrconsts.pas
    ===================================================================
    --- ide/lazarusidestrconsts.pas	(revision 60489)
    +++ ide/lazarusidestrconsts.pas	(working copy)
    @@ -2692,6 +2692,8 @@
       lisIdeMaintainsTheTitleInMainUnit = 'The IDE maintains the title in main unit.';
       lisMainUnitHasApplicationScaledStatement = 'Main unit has Application.Scaled statement';
       lisIdeMaintainsScaledInMainUnit = 'The IDE maintains Application.Scaled (Hi-DPI) in main unit.';
    +  lisLPICompatibilityModeCheckBox = 'Maximize compatibility of project files (LPI and LPS)';
    +  lisLPICompatibilityModeCheckBoxHint = 'Check this if you want to open your project in legacy (2.0 and older) Lazarus versions.';
       lisProjectIsRunnable = 'Project is runnable';
       lisProjectIsRunnableHint = 'Generates a binary executable which can be run.';
       lisUseDesignTimePackages = 'Use design time packages';
    Index: ide/project.pp
    ===================================================================
    --- ide/project.pp	(revision 60489)
    +++ ide/project.pp	(working copy)
    @@ -785,6 +785,7 @@
         function GetSourceDirectories: TFileReferenceList;
         function GetTargetFilename: string;
         function GetUnits(Index: integer): TUnitInfo;
    +    function GetUseLegacyLists: Boolean;
         function JumpHistoryCheckPosition(
                                     APosition:TProjectJumpHistoryPosition): boolean;
         function OnUnitFileBackup(const Filename: string): TModalResult;
    @@ -1058,6 +1059,7 @@
         property EnableI18NForLFM: boolean read FEnableI18NForLFM write SetEnableI18NForLFM;
         property I18NExcludedIdentifiers: TStrings read FI18NExcludedIdentifiers;
         property I18NExcludedOriginals: TStrings read FI18NExcludedOriginals;
    +    property UseLegacyLists: Boolean read GetUseLegacyLists;
         property ForceUpdatePoFiles: Boolean read FForceUpdatePoFiles write FForceUpdatePoFiles;
         property FirstAutoRevertLockedUnit: TUnitInfo read GetFirstAutoRevertLockedUnit;
         property FirstLoadedUnit: TUnitInfo read GetFirstLoadedUnit;
    @@ -1134,7 +1136,7 @@
     implementation
     
     const
    -  ProjectInfoFileVersion = 11;
    +  ProjectInfoFileVersion = 12;
       ProjOptionsPath = 'ProjectOptions/';
     
     
    @@ -2816,9 +2818,9 @@
       MergeUnitInfo: Boolean;
     begin
       {$IFDEF IDE_MEM_CHECK}CheckHeapWrtMemCnt('TProject.ReadProject D reading units');{$ENDIF}
    -  NewUnitCount:=FXMLConfig.GetValue(Path+'Units/Count',0);
    +  NewUnitCount:=FXMLConfig.GetListItemCount(Path+'Units/', UseLegacyLists);
       for i := 0 to NewUnitCount - 1 do begin
    -    SubPath:=Path+'Units/Unit'+IntToStr(i)+'/';
    +    SubPath:=Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, UseLegacyLists)+'/';
         NewUnitFilename:=FXMLConfig.GetValue(SubPath+'Filename/Value','');
         OnLoadSaveFilename(NewUnitFilename,true);
         // load unit and add it
    @@ -2867,7 +2869,7 @@
     const
       Path = ProjOptionsPath;
     begin
    -  if (FFileVersion=0) and (FXMLConfig.GetValue(Path+'Units/Count',0)=0) then
    +  if (FFileVersion=0) and (FXMLConfig.GetListItemCount(Path+'Units/', UseLegacyLists)=0) then
         if IDEMessageDialog(lisStrangeLpiFile,
             Format(lisTheFileDoesNotLookLikeALpiFile, [ProjectInfoFile]),
             mtConfirmation,[mbIgnore,mbAbort])<>mrIgnore
    @@ -3149,10 +3151,10 @@
       for i:=0 to UnitCount-1 do
         if UnitMustBeSaved(Units[i],FProjectWriteFlags,SaveSession) then begin
           Units[i].SaveToXMLConfig(FXMLConfig,
    -        Path+'Units/Unit'+IntToStr(SaveUnitCount)+'/',True,SaveSession,fCurStorePathDelim);
    +        Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, UseLegacyLists)+'/',True,SaveSession,fCurStorePathDelim);
           inc(SaveUnitCount);
         end;
    -  FXMLConfig.SetDeleteValue(Path+'Units/Count',SaveUnitCount,0);
    +  FXMLConfig.SetListItemCount(Path+'Units/',SaveUnitCount,UseLegacyLists);
     end;
     
     procedure TProject.SaveOtherDefines(const Path: string);
    @@ -4453,6 +4455,11 @@
       end;
     end;
     
    +function TProject.GetUseLegacyLists: Boolean;
    +begin
    +  Result := (FFileVersion<=11) or (pfCompatibilityMode in Flags);
    +end;
    +
     function TProject.HasProjectInfoFileChangedOnDisk: boolean;
     var
       AnUnitInfo: TUnitInfo;
    
  • xmlcfg-nodeindexes-03.patch (29,839 bytes)
    Index: components/ideintf/projectintf.pas
    ===================================================================
    --- components/ideintf/projectintf.pas	(revision 60489)
    +++ components/ideintf/projectintf.pas	(working copy)
    @@ -247,7 +247,8 @@
         pfLRSFilesInOutputDirectory, // put .lrs files in output directory
         pfUseDefaultCompilerOptions, // load users default compiler options
         pfSaveJumpHistory,
    -    pfSaveFoldState
    +    pfSaveFoldState,
    +    pfCompatibilityMode // use legacy file format to maximize compatibility with old Lazarus versions
         );
       TProjectFlags = set of TProjectFlag;
     
    @@ -274,7 +275,8 @@
           'LRSInOutputDirectory',
           'UseDefaultCompilerOptions',
           'SaveJumpHistory',
    -      'SaveFoldState'
    +      'SaveFoldState',
    +      'CompatibilityMode'
         );
       ProjectSessionStorageNames: array[TProjectSessionStorage] of string = (
         'InProjectInfo',
    Index: components/lazutils/laz2_xmlcfg.pas
    ===================================================================
    --- components/lazutils/laz2_xmlcfg.pas	(revision 60489)
    +++ components/lazutils/laz2_xmlcfg.pas	(working copy)
    @@ -16,6 +16,7 @@
     }
     
     {$MODE objfpc}
    +{$modeswitch advancedrecords}
     {$H+}
     
     unit Laz2_XMLCfg;
    @@ -50,10 +51,23 @@
         procedure SetFilename(const AFilename: String);
       protected
         type
    +      TDomNodeArray = array of TDomNode;
           TNodeCache = record
             Node: TDomNode;
    +        NodeSearchName: string;
             ChildrenValid: boolean;
    -        Children: array of TDomNode; // nodes with NodeName<>'' and sorted
    +        Children: TDomNodeArray; // child nodes with NodeName<>'' and sorted
    +
    +        NodeListName: string;
    +        NodeList: TDomNodeArray; // child nodes that are accessed with "name[?]" XPath
    +
    +      public
    +        class procedure GrowArray(var aArray: TDomNodeArray; aCount: Integer); static;
    +        procedure RefreshChildren;
    +        procedure RefreshChildrenIfNeeded;
    +        procedure RefreshNodeList(const ANodeName: string);
    +        procedure RefreshNodeListIfNeeded(const ANodeName: string);
    +        function AddNodeToList: TDOMNode;
           end;
       protected
         doc: TXMLDocument;
    @@ -68,13 +82,15 @@
         procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); virtual;
         procedure WriteXMLFile(ADoc: TXMLDocument; const AFileName: String); virtual;
         procedure FreeDoc; virtual;
    -    procedure SetPathNodeCache(Index: integer; aNode: TDomNode);
    +    procedure SetPathNodeCache(Index: integer; aNode: TDomNode; aNodeSearchName: string = '');
         function GetCachedPathNode(Index: integer): TDomNode; inline;
    +    function GetCachedPathNode(Index: integer; out aNodeSearchName: string): TDomNode; inline;
         procedure InvalidateCacheTilEnd(StartIndex: integer);
         function InternalFindNode(const APath: String; PathLen: integer;
                                   CreateNodes: boolean = false): TDomNode;
         procedure InternalCleanNode(Node: TDomNode);
    -    function FindChildNode(PathIndex: integer; const aName: string): TDomNode;
    +    function FindChildNode(PathIndex: integer; const aName: string;
    +      CreateNodes: boolean = false): TDomNode;
       public
         constructor Create(AOwner: TComponent); override; overload;
         constructor Create(const AFilename: String); overload; // create and load
    @@ -109,6 +125,12 @@
         // checks if the path has values, set PathHasValue=true to skip the last part
         function HasPath(const APath: string; PathHasValue: boolean): boolean;
         function HasChildPaths(const APath: string): boolean;
    +    function GetChildCount(const APath: string): Integer;
    +    function IsLegacyList(const APath: string): Boolean;
    +    function GetListItemCount(const APath, AItemName: string; const aLegacyList: Boolean): Integer;
    +    function GetListItemXPath(const AName: string; const AIndex: Integer; const aLegacyList: Boolean;
    +      const aLegacyList1Based: Boolean = False): string;
    +    procedure SetListItemCount(const APath: string; const ACount: Integer; const ALegacyList: Boolean);
         property Modified: Boolean read FModified write FModified;
         procedure InvalidatePathCache;
       published
    @@ -150,15 +172,121 @@
       Result:=CompareStr(Node1.NodeName,Node2.NodeName);
     end;
     
    +{ TXMLConfig.TNodeCache }
    +
    +function TXMLConfig.TNodeCache.AddNodeToList: TDOMNode;
    +begin
    +  Result:=Node.OwnerDocument.CreateElement(NodeListName);
    +  Node.AppendChild(Result);
    +  SetLength(NodeList, Length(NodeList)+1);
    +  NodeList[High(NodeList)]:=Result;
    +end;
    +
    +class procedure TXMLConfig.TNodeCache.GrowArray(var aArray: TDomNodeArray;
    +  aCount: Integer);
    +var
    +  cCount: Integer;
    +begin
    +  cCount:=length(aArray);
    +  if aCount>cCount then begin
    +    if cCount<8 then
    +      cCount:=8
    +    else
    +      cCount:=cCount*2;
    +    if aCount>cCount then
    +      cCount := aCount;
    +    SetLength(aArray,cCount);
    +  end;
    +end;
    +
    +procedure TXMLConfig.TNodeCache.RefreshChildren;
    +var
    +  aCount, m: Integer;
    +  aChild: TDOMNode;
    +begin
    +  // collect all children and sort
    +  aCount:=0;
    +  aChild:=Node.FirstChild;
    +  while aChild<>nil do begin
    +    if aChild.NodeName<>'' then begin
    +      GrowArray(Children, aCount+1);
    +      Children[aCount]:=aChild;
    +      inc(aCount);
    +    end;
    +    aChild:=aChild.NextSibling;
    +  end;
    +  SetLength(Children,aCount);
    +  if aCount>1 then
    +    MergeSortWithLen(@Children[0],aCount,@CompareDomNodeNames); // sort ascending [0]<[1]
    +  for m:=0 to aCount-2 do
    +    if Children[m].NodeName=Children[m+1].NodeName then begin
    +      // duplicate found: nodes with same name
    +      // -> use only the first
    +      Children[m+1]:=Children[m];
    +    end;
    +  ChildrenValid:=true;
    +end;
    +
    +procedure TXMLConfig.TNodeCache.RefreshChildrenIfNeeded;
    +begin
    +  if not ChildrenValid then
    +    RefreshChildren;
    +end;
    +
    +procedure TXMLConfig.TNodeCache.RefreshNodeList(const ANodeName: string);
    +var
    +  aCount: Integer;
    +  aChild: TDOMNode;
    +begin
    +  aCount:=0;
    +  aChild:=Node.FirstChild;
    +  while aChild<>nil do
    +  begin
    +    if aChild.NodeName=ANodeName then
    +    begin
    +      GrowArray(NodeList, aCount+1);
    +      NodeList[aCount]:=aChild;
    +      inc(aCount);
    +    end;
    +    aChild:=aChild.NextSibling;
    +  end;
    +  SetLength(NodeList,aCount);
    +  NodeListName := ANodeName;
    +end;
    +
    +procedure TXMLConfig.TNodeCache.RefreshNodeListIfNeeded(const ANodeName: string
    +  );
    +begin
    +  if NodeListName<>ANodeName then
    +    RefreshNodeList(ANodeName);
    +end;
    +
     // inline
    -function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
    +function TXMLConfig.GetCachedPathNode(Index: integer; out
    +  aNodeSearchName: string): TDomNode;
     begin
       if Index<length(fPathNodeCache) then
    -    Result:=fPathNodeCache[Index].Node
    -  else
    +  begin
    +    Result:=fPathNodeCache[Index].Node;
    +    aNodeSearchName:=fPathNodeCache[Index].NodeSearchName;
    +  end else
    +  begin
         Result:=nil;
    +    aNodeSearchName:='';
    +  end;
     end;
     
    +function TXMLConfig.GetChildCount(const APath: string): Integer;
    +var
    +  Node: TDOMNode;
    +begin
    +  Node:=FindNode(APath,false);
    +  if Node=nil then
    +    Result := 0
    +  else
    +    Result := Node.GetChildCount;
    +end;
    +
     constructor TXMLConfig.Create(const AFilename: String);
     begin
       //DebugLn(['TXMLConfig.Create ',AFilename]);
    @@ -294,6 +422,41 @@
       Result:=StrToExtended(GetValue(APath,''),ADefault);
     end;
     
    +function TXMLConfig.GetListItemCount(const APath, AItemName: string;
    +  const aLegacyList: Boolean): Integer;
    +var
    +  Node: TDOMNode;
    +  NodeLevel: SizeInt;
    +begin
    +  if aLegacyList then
    +    Result := GetValue(APath+'Count',0)
    +  else
    +  begin
    +    Node:=InternalFindNode(APath,Length(APath));
    +    if Node<>nil then
    +    begin
    +      NodeLevel := Node.GetLevel-1;
    +      fPathNodeCache[NodeLevel].RefreshNodeListIfNeeded(AItemName);
    +      Result := Length(fPathNodeCache[NodeLevel].NodeList);
    +    end else
    +      Result := 0;
    +  end;
    +end;
    +
    +function TXMLConfig.GetListItemXPath(const AName: string;
    +  const AIndex: Integer; const aLegacyList: Boolean;
    +  const aLegacyList1Based: Boolean): string;
    +begin
    +  if ALegacyList then
    +  begin
    +    if aLegacyList1Based then
    +      Result := AName+IntToStr(AIndex+1)
    +    else
    +      Result := AName+IntToStr(AIndex);
    +  end else
    +    Result := AName+'['+IntToStr(AIndex+1)+']';
    +end;
    +
     procedure TXMLConfig.SetValue(const APath, AValue: String);
     var
       Node: TDOMNode;
    @@ -450,6 +613,11 @@
       InvalidateCacheTilEnd(0);
     end;
     
    +function TXMLConfig.IsLegacyList(const APath: string): Boolean;
    +begin
    +  Result := GetValue(APath+'Count',-1)>1;
    +end;
    +
     function TXMLConfig.ExtendedToStr(const e: extended): string;
     begin
       Result := FloatToStr(e, FPointSettings);
    @@ -478,8 +646,16 @@
       FreeAndNil(doc);
     end;
     
    -procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode);
    +function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
     var
    +  x: string;
    +begin
    +  Result := GetCachedPathNode(Index, x);
    +end;
    +
    +procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode;
    +  aNodeSearchName: string);
    +var
       OldLength, NewLength: Integer;
     begin
       OldLength:=length(fPathNodeCache);
    @@ -495,9 +671,13 @@
         exit
       else
         InvalidateCacheTilEnd(Index+1);
    +  if aNodeSearchName='' then
    +    aNodeSearchName:=aNode.NodeName;
       with fPathNodeCache[Index] do begin
         Node:=aNode;
    +    NodeSearchName:=aNodeSearchName;
         ChildrenValid:=false;
    +    NodeListName:='';
       end;
     end;
     
    @@ -510,6 +690,7 @@
           if Node=nil then break;
           Node:=nil;
           ChildrenValid:=false;
    +      NodeListName:='';
         end;
       end;
     end;
    @@ -517,11 +698,9 @@
     function TXMLConfig.InternalFindNode(const APath: String; PathLen: integer;
       CreateNodes: boolean): TDomNode;
     var
    -  NodePath: String;
    +  NodePath, NdName: String;
       StartPos, EndPos: integer;
       PathIndex: Integer;
    -  Parent: TDOMNode;
    -  NdName: DOMString;
       NameLen: Integer;
     begin
       //debugln(['TXMLConfig.InternalFindNode APath="',copy(APath,1,PathLen),'" CreateNodes=',CreateNodes]);
    @@ -539,25 +718,15 @@
         NameLen:=EndPos-StartPos;
         if NameLen=0 then break;
         inc(PathIndex);
    -    Parent:=Result;
    -    Result:=GetCachedPathNode(PathIndex);
    -    if Result<>nil then
    -      NdName:=Result.NodeName;
    +    Result:=GetCachedPathNode(PathIndex,NdName);
         if (Result=nil) or (length(NdName)<>NameLen)
         or not CompareMem(PChar(NdName),@APath[StartPos],NameLen) then begin
           // different path => search
           NodePath:=copy(APath,StartPos,NameLen);
    -      Result:=FindChildNode(PathIndex-1,NodePath);
    -      if Result=nil then begin
    -        if not CreateNodes then exit;
    -        // create missing node
    -        Result:=Doc.CreateElement(NodePath);
    -        Parent.AppendChild(Result);
    -        fPathNodeCache[PathIndex-1].ChildrenValid:=false;
    -        InvalidateCacheTilEnd(PathIndex);
    -        if EndPos>PathLen then exit;
    -      end;
    -      SetPathNodeCache(PathIndex,Result);
    +      Result:=FindChildNode(PathIndex-1,NodePath,CreateNodes);
    +      if Result=nil then
    +        Exit;
    +      SetPathNodeCache(PathIndex,Result,NodePath);
         end;
         StartPos:=EndPos+1;
         if StartPos>PathLen then exit;
    @@ -581,62 +750,56 @@
       end;
     end;
     
    -function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string
    -  ): TDomNode;
    +function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string;
    +  CreateNodes: boolean): TDomNode;
     var
    -  aParent, aChild: TDOMNode;
    -  aCount: Integer;
    -  NewLength: Integer;
       l, r, m: Integer;
    -  cmp: Integer;
    +  cmp, BrPos: Integer;
    +  NodeName: string;
     begin
    -  with fPathNodeCache[PathIndex] do begin
    -    if not ChildrenValid then begin
    -      // collect all children and sort
    -      aParent:=Node;
    -      aCount:=0;
    -      aChild:=aParent.FirstChild;
    -      while aChild<>nil do begin
    -        if aChild.NodeName<>'' then begin
    -          if aCount=length(Children) then begin
    -            NewLength:=length(Children);
    -            if NewLength<8 then
    -              NewLength:=8
    -            else
    -              NewLength:=NewLength*2;
    -            SetLength(Children,NewLength);
    -          end;
    -          Children[aCount]:=aChild;
    -          inc(aCount);
    -        end;
    -        aChild:=aChild.NextSibling;
    -      end;
    -      SetLength(Children,aCount);
    -      if aCount>1 then
    -        MergeSortWithLen(@Children[0],aCount,@CompareDomNodeNames); // sort ascending [0]<[1]
    -      for m:=0 to aCount-2 do
    -        if Children[m].NodeName=Children[m+1].NodeName then begin
    -          // duplicate found: nodes with same name
    -          // -> use only the first
    -          Children[m+1]:=Children[m];
    -        end;
    -      ChildrenValid:=true;
    +  BrPos := Pos('[', aName);
    +  if (Length(aName)>=BrPos+2) and (aName[Length(aName)]=']')
    +  and TryStrToInt(Trim(Copy(aName, BrPos+1, Length(aName)-BrPos-1)), m) then
    +  begin
    +    // support XPath in format "name[?]"
    +    NodeName := Trim(Copy(aName, 1, BrPos-1));
    +    fPathNodeCache[PathIndex].RefreshNodeListIfNeeded(NodeName);
    +    if m<=0 then
    +      raise Exception.CreateFmt('Invalid node index in XPath descriptor "%s".', [aName])
    +    else if (m<=Length(fPathNodeCache[PathIndex].NodeList)) then
    +      Result:=fPathNodeCache[PathIndex].NodeList[m-1]
    +    else if CreateNodes then
    +    begin
    +      for l := Length(fPathNodeCache[PathIndex].NodeList)+1 to m do
    +        Result := fPathNodeCache[PathIndex].AddNodeToList;
    +      InvalidateCacheTilEnd(PathIndex+1);
         end;
    +  end else
    +  begin
    +    fPathNodeCache[PathIndex].RefreshChildrenIfNeeded;
     
         // binary search
         l:=0;
    -    r:=length(Children)-1;
    +    r:=length(fPathNodeCache[PathIndex].Children)-1;
         while l<=r do begin
           m:=(l+r) shr 1;
    -      cmp:=CompareStr(aName,Children[m].NodeName);
    +      cmp:=CompareStr(aName,fPathNodeCache[PathIndex].Children[m].NodeName);
           if cmp<0 then
             r:=m-1
           else if cmp>0 then
             l:=m+1
           else
    -        exit(Children[m]);
    +        exit(fPathNodeCache[PathIndex].Children[m]);
         end;
    -    Result:=nil;
    +    if CreateNodes then
    +    begin
    +      // create missing node
    +      Result:=Doc.CreateElement(aName);
    +      fPathNodeCache[PathIndex].Node.AppendChild(Result);
    +      fPathNodeCache[PathIndex].ChildrenValid:=false;
    +      InvalidateCacheTilEnd(PathIndex+1);
    +    end else
    +      Result:=nil;
       end;
     end;
     
    @@ -686,6 +849,13 @@
       {$IFDEF MEM_CHECK}CheckHeapWrtMemCnt('TXMLConfig.SetFilename END');{$ENDIF}
     end;
     
    +procedure TXMLConfig.SetListItemCount(const APath: string;
    +  const ACount: Integer; const ALegacyList: Boolean);
    +begin
    +  if ALegacyList then
    +    SetDeleteValue(APath+'Count',ACount,0)
    +end;
    +
     procedure TXMLConfig.CreateConfigNode;
     var
       cfg: TDOMElement;
    Index: ide/frames/project_misc_options.lfm
    ===================================================================
    --- ide/frames/project_misc_options.lfm	(revision 60489)
    +++ ide/frames/project_misc_options.lfm	(working copy)
    @@ -118,13 +118,13 @@
       end
       object ResourceGroupBox: TGroupBox
         AnchorSideLeft.Control = Owner
    -    AnchorSideTop.Control = LRSInOutputDirCheckBox
    +    AnchorSideTop.Control = CompatibilityModeCheckBox
         AnchorSideTop.Side = asrBottom
         AnchorSideRight.Control = Owner
         AnchorSideRight.Side = asrBottom
         Left = 0
         Height = 81
    -    Top = 231
    +    Top = 256
         Width = 536
         Anchors = [akTop, akLeft, akRight]
         BorderSpacing.Top = 6
    @@ -139,7 +139,7 @@
         ChildSizing.ControlsPerLine = 1
         ClientHeight = 61
         ClientWidth = 532
    -    TabOrder = 9
    +    TabOrder = 10
         object UseFPCResourcesRadioButton: TRadioButton
           Left = 6
           Height = 25
    @@ -183,7 +183,7 @@
         AnchorSideTop.Side = asrCenter
         Left = 0
         Height = 15
    -    Top = 331
    +    Top = 356
         Width = 83
         Caption = 'PathDelimLabel'
         ParentColor = False
    @@ -196,7 +196,7 @@
         AnchorSideRight.Side = asrBottom
         Left = 0
         Height = 3
    -    Top = 318
    +    Top = 343
         Width = 536
         Anchors = [akTop, akLeft, akRight]
         BorderSpacing.Top = 6
    @@ -209,12 +209,12 @@
         AnchorSideRight.Side = asrBottom
         Left = 89
         Height = 23
    -    Top = 327
    +    Top = 352
         Width = 259
         BorderSpacing.Left = 6
         BorderSpacing.Top = 6
         ItemHeight = 15
    -    TabOrder = 10
    +    TabOrder = 11
         Text = 'PathDelimComboBox'
       end
       object MainUnitHasScaledStatementCheckBox: TCheckBox
    @@ -231,4 +231,18 @@
         ShowHint = True
         TabOrder = 4
       end
    +  object CompatibilityModeCheckBox: TCheckBox
    +    AnchorSideLeft.Control = Owner
    +    AnchorSideTop.Control = LRSInOutputDirCheckBox
    +    AnchorSideTop.Side = asrBottom
    +    Left = 0
    +    Height = 19
    +    Top = 231
    +    Width = 175
    +    BorderSpacing.Top = 6
    +    Caption = 'CompatibilityModeCheckBox'
    +    ParentShowHint = False
    +    ShowHint = True
    +    TabOrder = 9
    +  end
     end
    Index: ide/frames/project_misc_options.pas
    ===================================================================
    --- ide/frames/project_misc_options.pas	(revision 60489)
    +++ ide/frames/project_misc_options.pas	(working copy)
    @@ -25,6 +25,7 @@
         Bevel2: TBevel;
         LRSInOutputDirCheckBox: TCheckBox;
         MainUnitHasCreateFormStatementsCheckBox: TCheckBox;
    +    CompatibilityModeCheckBox: TCheckBox;
         MainUnitHasTitleStatementCheckBox: TCheckBox;
         MainUnitHasScaledStatementCheckBox: TCheckBox;
         MainUnitHasUsesSectionForAllUnitsCheckBox: TCheckBox;
    @@ -68,6 +69,8 @@
       MainUnitHasTitleStatementCheckBox.Hint := lisIdeMaintainsTheTitleInMainUnit;
       MainUnitHasScaledStatementCheckBox.Caption := lisMainUnitHasApplicationScaledStatement;
       MainUnitHasScaledStatementCheckBox.Hint := lisIdeMaintainsScaledInMainUnit;
    +  CompatibilityModeCheckBox.Caption := lisLPICompatibilityModeCheckBox;
    +  CompatibilityModeCheckBox.Hint := lisLPICompatibilityModeCheckBoxHint;
       RunnableCheckBox.Caption := lisProjectIsRunnable;
       RunnableCheckBox.Hint := lisProjectIsRunnableHint;
       UseDesignTimePkgsCheckBox.Caption := lisUseDesignTimePackages;
    @@ -96,6 +99,7 @@
         MainUnitHasCreateFormStatementsCheckBox.Checked := (pfMainUnitHasCreateFormStatements in Flags);
         MainUnitHasTitleStatementCheckBox.Checked := (pfMainUnitHasTitleStatement in Flags);
         MainUnitHasScaledStatementCheckBox.Checked := (pfMainUnitHasScaledStatement in Flags);
    +    CompatibilityModeCheckBox.Checked := (pfCompatibilityMode in Flags);
         RunnableCheckBox.Checked := (pfRunnable in Flags);
         UseDesignTimePkgsCheckBox.Checked := (pfUseDesignTimePackages in Flags);
         AlwaysBuildCheckBox.Checked := (pfAlwaysBuild in Flags);
    @@ -140,6 +144,8 @@
                      MainUnitHasTitleStatementCheckBox.Checked);
       SetProjectFlag(pfMainUnitHasScaledStatement,
                      MainUnitHasScaledStatementCheckBox.Checked);
    +  SetProjectFlag(pfCompatibilityMode,
    +                 CompatibilityModeCheckBox.Checked);
       SetProjectFlag(pfRunnable, RunnableCheckBox.Checked);
       SetProjectFlag(pfUseDesignTimePackages, UseDesignTimePkgsCheckBox.Checked);
       SetProjectFlag(pfAlwaysBuild, AlwaysBuildCheckBox.Checked);
    Index: ide/imexportcompileropts.pas
    ===================================================================
    --- ide/imexportcompileropts.pas	(revision 60489)
    +++ ide/imexportcompileropts.pas	(working copy)
    @@ -262,7 +262,7 @@
     begin
       Result := OpenXML(Filename);
       if Result <> mrOK then Exit;
    -  Project1.BuildModes.SaveProjOptsToXMLConfig(fXMLConfig, '', False);
    +  Project1.BuildModes.SaveProjOptsToXMLConfig(fXMLConfig, '', False, True);
       fXMLConfig.Flush;
       ShowMessageFmt(lisSuccessfullyExportedBuildModes, [Project1.BuildModes.Count, Filename]);
     end;
    Index: ide/lazarusidestrconsts.pas
    ===================================================================
    --- ide/lazarusidestrconsts.pas	(revision 60489)
    +++ ide/lazarusidestrconsts.pas	(working copy)
    @@ -2692,6 +2692,8 @@
       lisIdeMaintainsTheTitleInMainUnit = 'The IDE maintains the title in main unit.';
       lisMainUnitHasApplicationScaledStatement = 'Main unit has Application.Scaled statement';
       lisIdeMaintainsScaledInMainUnit = 'The IDE maintains Application.Scaled (Hi-DPI) in main unit.';
    +  lisLPICompatibilityModeCheckBox = 'Maximize compatibility of project files (LPI and LPS)';
    +  lisLPICompatibilityModeCheckBoxHint = 'Check this if you want to open your project in legacy (2.0 and older) Lazarus versions.';
       lisProjectIsRunnable = 'Project is runnable';
       lisProjectIsRunnableHint = 'Generates a binary executable which can be run.';
       lisUseDesignTimePackages = 'Use design time packages';
    Index: ide/project.pp
    ===================================================================
    --- ide/project.pp	(revision 60489)
    +++ ide/project.pp	(working copy)
    @@ -589,7 +589,7 @@
         procedure LoadFromXMLConfig(XMLConfig: TXMLConfig; const Path: string);
         procedure SaveMacroValuesAtOldPlace(XMLConfig: TXMLConfig; const Path: string);
         procedure SaveToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
    -                              IsDefault: Boolean; var Cnt: integer);
    +                              IsDefault, ALegacyList: Boolean; var Cnt: integer);
         function GetCaption: string; override;
         function GetIndex: integer; override;
       public
    @@ -655,9 +655,9 @@
         procedure LoadSessionFromXMLConfig(XMLConfig: TXMLConfig; const Path: string;
                                            LoadAllOptions: boolean);
         procedure SaveProjOptsToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
    -                                      SaveSession: boolean);
    +                                      SaveSession, ALegacyList: boolean);
         procedure SaveSessionOptsToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
    -                                         SaveSession: boolean);
    +                                         SaveSession, ALegacyList: boolean);
       public
         property Items[Index: integer]: TProjectBuildMode read GetItems; default;
         property ChangeStamp: integer read FChangeStamp;
    @@ -785,6 +785,7 @@
         function GetSourceDirectories: TFileReferenceList;
         function GetTargetFilename: string;
         function GetUnits(Index: integer): TUnitInfo;
    +    function GetUseLegacyLists: Boolean;
         function JumpHistoryCheckPosition(
                                     APosition:TProjectJumpHistoryPosition): boolean;
         function OnUnitFileBackup(const Filename: string): TModalResult;
    @@ -1058,6 +1059,7 @@
         property EnableI18NForLFM: boolean read FEnableI18NForLFM write SetEnableI18NForLFM;
         property I18NExcludedIdentifiers: TStrings read FI18NExcludedIdentifiers;
         property I18NExcludedOriginals: TStrings read FI18NExcludedOriginals;
    +    property UseLegacyLists: Boolean read GetUseLegacyLists;
         property ForceUpdatePoFiles: Boolean read FForceUpdatePoFiles write FForceUpdatePoFiles;
         property FirstAutoRevertLockedUnit: TUnitInfo read GetFirstAutoRevertLockedUnit;
         property FirstLoadedUnit: TUnitInfo read GetFirstLoadedUnit;
    @@ -1134,7 +1136,7 @@
     implementation
     
     const
    -  ProjectInfoFileVersion = 11;
    +  ProjectInfoFileVersion = 12;
       ProjOptionsPath = 'ProjectOptions/';
     
     
    @@ -2813,12 +2815,13 @@
       SubPath: String;
       NewUnitFilename: String;
       OldUnitInfo: TUnitInfo;
    -  MergeUnitInfo: Boolean;
    +  MergeUnitInfo, LegacyList: Boolean;
     begin
       {$IFDEF IDE_MEM_CHECK}CheckHeapWrtMemCnt('TProject.ReadProject D reading units');{$ENDIF}
    -  NewUnitCount:=FXMLConfig.GetValue(Path+'Units/Count',0);
    +  LegacyList:=(FFileVersion<=11) or FXMLConfig.IsLegacyList(Path+'Units/');
    +  NewUnitCount:=FXMLConfig.GetListItemCount(Path+'Units/', 'Unit', LegacyList);
       for i := 0 to NewUnitCount - 1 do begin
    -    SubPath:=Path+'Units/Unit'+IntToStr(i)+'/';
    +    SubPath:=Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, LegacyList)+'/';
         NewUnitFilename:=FXMLConfig.GetValue(SubPath+'Filename/Value','');
         OnLoadSaveFilename(NewUnitFilename,true);
         // load unit and add it
    @@ -2867,7 +2870,7 @@
     const
       Path = ProjOptionsPath;
     begin
    -  if (FFileVersion=0) and (FXMLConfig.GetValue(Path+'Units/Count',0)=0) then
    +  if (FFileVersion=0) and (FXMLConfig.GetListItemCount(Path+'Units/', 'Unit', UseLegacyLists)=0) then
         if IDEMessageDialog(lisStrangeLpiFile,
             Format(lisTheFileDoesNotLookLikeALpiFile, [ProjectInfoFile]),
             mtConfirmation,[mbIgnore,mbAbort])<>mrIgnore
    @@ -3149,10 +3152,10 @@
       for i:=0 to UnitCount-1 do
         if UnitMustBeSaved(Units[i],FProjectWriteFlags,SaveSession) then begin
           Units[i].SaveToXMLConfig(FXMLConfig,
    -        Path+'Units/Unit'+IntToStr(SaveUnitCount)+'/',True,SaveSession,fCurStorePathDelim);
    +        Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, UseLegacyLists)+'/',True,SaveSession,fCurStorePathDelim);
           inc(SaveUnitCount);
         end;
    -  FXMLConfig.SetDeleteValue(Path+'Units/Count',SaveUnitCount,0);
    +  FXMLConfig.SetListItemCount(Path+'Units/',SaveUnitCount,UseLegacyLists);
     end;
     
     procedure TProject.SaveOtherDefines(const Path: string);
    @@ -3232,7 +3235,7 @@
       // save custom data
       SaveStringToStringTree(FXMLConfig,CustomData,Path+'CustomData/');
       // Save the macro values and compiler options
    -  BuildModes.SaveProjOptsToXMLConfig(FXMLConfig, Path, FSaveSessionInLPI);
    +  BuildModes.SaveProjOptsToXMLConfig(FXMLConfig, Path, FSaveSessionInLPI, UseLegacyLists);
       BuildModes.SaveSharedMatrixOptions(Path);
       if FSaveSessionInLPI then
         BuildModes.SaveSessionData(Path);
    @@ -3293,7 +3296,7 @@
       FXMLConfig.SetValue(Path+'Version/Value',ProjectInfoFileVersion);
     
       // Save the session build modes
    -  BuildModes.SaveSessionOptsToXMLConfig(FXMLConfig, Path, True);
    +  BuildModes.SaveSessionOptsToXMLConfig(FXMLConfig, Path, True, UseLegacyLists);
       BuildModes.SaveSessionData(Path);
       // save all units
       SaveUnits(Path,true);
    @@ -4453,6 +4456,11 @@
       end;
     end;
     
    +function TProject.GetUseLegacyLists: Boolean;
    +begin
    +  Result := (FFileVersion<=11) or (pfCompatibilityMode in Flags);
    +end;
    +
     function TProject.HasProjectInfoFileChangedOnDisk: boolean;
     var
       AnUnitInfo: TUnitInfo;
    @@ -6814,13 +6822,13 @@
       XMLConfig.SetDeleteValue(Path+'Count',Cnt,0);
     end;
     
    -procedure TProjectBuildMode.SaveToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
    -  IsDefault: Boolean; var Cnt: integer);
    +procedure TProjectBuildMode.SaveToXMLConfig(XMLConfig: TXMLConfig;
    +  const Path: string; IsDefault, ALegacyList: Boolean; var Cnt: integer);
     var
       SubPath: String;
     begin
    +  SubPath:=Path+'BuildModes/'+XMLConfig.GetListItemXPath('Item', Cnt, ALegacyList, True)+'/';
       inc(Cnt);
    -  SubPath:=Path+'BuildModes/Item'+IntToStr(Cnt)+'/';
       XMLConfig.SetDeleteValue(SubPath+'Name',Identifier,'');
       if IsDefault then
         XMLConfig.SetDeleteValue(SubPath+'Default',True,false)
    @@ -7198,10 +7206,12 @@
       i: Integer;
       Ident, SubPath: String;
       CurMode: TProjectBuildMode;
    +  LegacyList: Boolean;
     begin
    +  LegacyList := FXMLConfig.IsLegacyList(Path);
       for i:=FromIndex to ToIndex do
       begin
    -    SubPath:=Path+'Item'+IntToStr(i)+'/';
    +    SubPath:=Path+FXMLConfig.GetListItemXPath('Item', i-1, LegacyList, True)+'/';
         Ident:=FXMLConfig.GetValue(SubPath+'Name','');
         CurMode:=Add(Ident);                     // add another mode
         CurMode.InSession:=InSession;
    @@ -7231,13 +7241,15 @@
     var
       i: Integer;
       SubPath: String;
    +  IsLegacyList: Boolean;
     begin
       // First default mode.
       LoadMacroValues(Path+'MacroValues/', Items[0]);
    +  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes');
       // Iterate rest of the modes.
       for i:=2 to Cnt do
       begin
    -    SubPath:=Path+'BuildModes/Item'+IntToStr(i)+'/';
    +    SubPath:=Path+'BuildModes/'+FXMLConfig.GetListItemXPath('Item', i-1, IsLegacyList, True);
         LoadMacroValues(SubPath+'MacroValues/', Items[i-1]);
       end;
     end;
    @@ -7279,15 +7291,17 @@
     // Load for project
     var
       Cnt: Integer;
    +  IsLegacyList: Boolean;
     begin
       FXMLConfig := XMLConfig;
     
    -  Cnt:=FXMLConfig.GetValue(Path+'BuildModes/Count',0);
    +  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes');
    +  Cnt:=FXMLConfig.GetListItemCount(Path+'BuildModes/', 'Item', IsLegacyList);
       if Cnt>0 then begin
         // Project default mode is stored at the old XML path for backward compatibility.
         // Testing the 'Default' XML attribute is not needed because the first mode
         // is always default.
    -    Items[0].Identifier:=FXMLConfig.GetValue(Path+'BuildModes/Item1/Name', '');
    +    Items[0].Identifier:=FXMLConfig.GetValue(Path+'BuildModes/'+XMLConfig.GetListItemXPath('Item', 0, IsLegacyList, True)+'/Name', '');
         Items[0].CompilerOptions.LoadFromXMLConfig(FXMLConfig, 'CompilerOptions/');
         LoadOtherCompilerOpts(Path+'BuildModes/', 2, Cnt, False);
         LoadAllMacroValues(Path+'MacroValues/', Cnt);
    @@ -7362,7 +7376,7 @@
     
     // SaveToXMLConfig itself
     procedure TProjectBuildModes.SaveProjOptsToXMLConfig(XMLConfig: TXMLConfig;
    -  const Path: string; SaveSession: boolean);
    +  const Path: string; SaveSession, ALegacyList: boolean);
     var
       i, Cnt: Integer;
     begin
    @@ -7377,12 +7391,12 @@
       Cnt:=0;
       for i:=0 to Count-1 do
         if SaveSession or not Items[i].InSession then
    -      Items[i].SaveToXMLConfig(FXMLConfig, Path, i=0, Cnt);
    -  FXMLConfig.SetDeleteValue(Path+'BuildModes/Count',Cnt,0);
    +      Items[i].SaveToXMLConfig(FXMLConfig, Path, i=0, ALegacyList, Cnt);
    +  FXMLConfig.SetListItemCount(Path+'BuildModes',Cnt,ALegacyList);
     end;
     
     procedure TProjectBuildModes.SaveSessionOptsToXMLConfig(XMLConfig: TXMLConfig;
    -  const Path: string; SaveSession: boolean);
    +  const Path: string; SaveSession, ALegacyList: boolean);
     var
       i, Cnt: Integer;
     begin
    @@ -7391,8 +7405,8 @@
       Cnt:=0;
       for i:=0 to Count-1 do
         if Items[i].InSession and SaveSession then
    -      Items[i].SaveToXMLConfig(FXMLConfig, Path, false, Cnt);
    -  FXMLConfig.SetDeleteValue(Path+'BuildModes/Count',Cnt,0);
    +      Items[i].SaveToXMLConfig(FXMLConfig, Path, false, ALegacyList, Cnt);
    +  FXMLConfig.SetListItemCount(Path+'BuildModes',Cnt,ALegacyList);
     end;
     
     
    
  • xmlcfg-nodeindexes-04.patch (31,032 bytes)
    Index: components/ideintf/projectintf.pas
    ===================================================================
    --- components/ideintf/projectintf.pas	(revision 60489)
    +++ components/ideintf/projectintf.pas	(working copy)
    @@ -247,7 +247,8 @@
         pfLRSFilesInOutputDirectory, // put .lrs files in output directory
         pfUseDefaultCompilerOptions, // load users default compiler options
         pfSaveJumpHistory,
    -    pfSaveFoldState
    +    pfSaveFoldState,
    +    pfCompatibilityMode // use legacy file format to maximize compatibility with old Lazarus versions
         );
       TProjectFlags = set of TProjectFlag;
     
    @@ -274,7 +275,8 @@
           'LRSInOutputDirectory',
           'UseDefaultCompilerOptions',
           'SaveJumpHistory',
    -      'SaveFoldState'
    +      'SaveFoldState',
    +      'CompatibilityMode'
         );
       ProjectSessionStorageNames: array[TProjectSessionStorage] of string = (
         'InProjectInfo',
    Index: components/lazutils/laz2_xmlcfg.pas
    ===================================================================
    --- components/lazutils/laz2_xmlcfg.pas	(revision 60489)
    +++ components/lazutils/laz2_xmlcfg.pas	(working copy)
    @@ -16,6 +16,7 @@
     }
     
     {$MODE objfpc}
    +{$modeswitch advancedrecords}
     {$H+}
     
     unit Laz2_XMLCfg;
    @@ -50,10 +51,23 @@
         procedure SetFilename(const AFilename: String);
       protected
         type
    +      TDomNodeArray = array of TDomNode;
           TNodeCache = record
             Node: TDomNode;
    +        NodeSearchName: string;
             ChildrenValid: boolean;
    -        Children: array of TDomNode; // nodes with NodeName<>'' and sorted
    +        Children: TDomNodeArray; // child nodes with NodeName<>'' and sorted
    +
    +        NodeListName: string;
    +        NodeList: TDomNodeArray; // child nodes that are accessed with "name[?]" XPath
    +
    +      public
    +        class procedure GrowArray(var aArray: TDomNodeArray; aCount: Integer); static;
    +        procedure RefreshChildren;
    +        procedure RefreshChildrenIfNeeded;
    +        procedure RefreshNodeList(const ANodeName: string);
    +        procedure RefreshNodeListIfNeeded(const ANodeName: string);
    +        function AddNodeToList: TDOMNode;
           end;
       protected
         doc: TXMLDocument;
    @@ -68,13 +82,15 @@
         procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); virtual;
         procedure WriteXMLFile(ADoc: TXMLDocument; const AFileName: String); virtual;
         procedure FreeDoc; virtual;
    -    procedure SetPathNodeCache(Index: integer; aNode: TDomNode);
    +    procedure SetPathNodeCache(Index: integer; aNode: TDomNode; aNodeSearchName: string = '');
         function GetCachedPathNode(Index: integer): TDomNode; inline;
    +    function GetCachedPathNode(Index: integer; out aNodeSearchName: string): TDomNode; inline;
         procedure InvalidateCacheTilEnd(StartIndex: integer);
         function InternalFindNode(const APath: String; PathLen: integer;
                                   CreateNodes: boolean = false): TDomNode;
         procedure InternalCleanNode(Node: TDomNode);
    -    function FindChildNode(PathIndex: integer; const aName: string): TDomNode;
    +    function FindChildNode(PathIndex: integer; const aName: string;
    +      CreateNodes: boolean = false): TDomNode;
       public
         constructor Create(AOwner: TComponent); override; overload;
         constructor Create(const AFilename: String); overload; // create and load
    @@ -109,6 +125,12 @@
         // checks if the path has values, set PathHasValue=true to skip the last part
         function HasPath(const APath: string; PathHasValue: boolean): boolean;
         function HasChildPaths(const APath: string): boolean;
    +    function GetChildCount(const APath: string): Integer;
    +    function IsLegacyList(const APath: string): Boolean;
    +    function GetListItemCount(const APath, AItemName: string; const aLegacyList: Boolean): Integer;
    +    function GetListItemXPath(const AName: string; const AIndex: Integer; const aLegacyList: Boolean;
    +      const aLegacyList1Based: Boolean = False): string;
    +    procedure SetListItemCount(const APath: string; const ACount: Integer; const ALegacyList: Boolean);
         property Modified: Boolean read FModified write FModified;
         procedure InvalidatePathCache;
       published
    @@ -150,15 +172,121 @@
       Result:=CompareStr(Node1.NodeName,Node2.NodeName);
     end;
     
    +{ TXMLConfig.TNodeCache }
    +
    +function TXMLConfig.TNodeCache.AddNodeToList: TDOMNode;
    +begin
    +  Result:=Node.OwnerDocument.CreateElement(NodeListName);
    +  Node.AppendChild(Result);
    +  SetLength(NodeList, Length(NodeList)+1);
    +  NodeList[High(NodeList)]:=Result;
    +end;
    +
    +class procedure TXMLConfig.TNodeCache.GrowArray(var aArray: TDomNodeArray;
    +  aCount: Integer);
    +var
    +  cCount: Integer;
    +begin
    +  cCount:=length(aArray);
    +  if aCount>cCount then begin
    +    if cCount<8 then
    +      cCount:=8
    +    else
    +      cCount:=cCount*2;
    +    if aCount>cCount then
    +      cCount := aCount;
    +    SetLength(aArray,cCount);
    +  end;
    +end;
    +
    +procedure TXMLConfig.TNodeCache.RefreshChildren;
    +var
    +  aCount, m: Integer;
    +  aChild: TDOMNode;
    +begin
    +  // collect all children and sort
    +  aCount:=0;
    +  aChild:=Node.FirstChild;
    +  while aChild<>nil do begin
    +    if aChild.NodeName<>'' then begin
    +      GrowArray(Children, aCount+1);
    +      Children[aCount]:=aChild;
    +      inc(aCount);
    +    end;
    +    aChild:=aChild.NextSibling;
    +  end;
    +  SetLength(Children,aCount);
    +  if aCount>1 then
    +    MergeSortWithLen(@Children[0],aCount,@CompareDomNodeNames); // sort ascending [0]<[1]
    +  for m:=0 to aCount-2 do
    +    if Children[m].NodeName=Children[m+1].NodeName then begin
    +      // duplicate found: nodes with same name
    +      // -> use only the first
    +      Children[m+1]:=Children[m];
    +    end;
    +  ChildrenValid:=true;
    +end;
    +
    +procedure TXMLConfig.TNodeCache.RefreshChildrenIfNeeded;
    +begin
    +  if not ChildrenValid then
    +    RefreshChildren;
    +end;
    +
    +procedure TXMLConfig.TNodeCache.RefreshNodeList(const ANodeName: string);
    +var
    +  aCount: Integer;
    +  aChild: TDOMNode;
    +begin
    +  aCount:=0;
    +  aChild:=Node.FirstChild;
    +  while aChild<>nil do
    +  begin
    +    if aChild.NodeName=ANodeName then
    +    begin
    +      GrowArray(NodeList, aCount+1);
    +      NodeList[aCount]:=aChild;
    +      inc(aCount);
    +    end;
    +    aChild:=aChild.NextSibling;
    +  end;
    +  SetLength(NodeList,aCount);
    +  NodeListName := ANodeName;
    +end;
    +
    +procedure TXMLConfig.TNodeCache.RefreshNodeListIfNeeded(const ANodeName: string
    +  );
    +begin
    +  if NodeListName<>ANodeName then
    +    RefreshNodeList(ANodeName);
    +end;
    +
     // inline
    -function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
    +function TXMLConfig.GetCachedPathNode(Index: integer; out
    +  aNodeSearchName: string): TDomNode;
     begin
       if Index<length(fPathNodeCache) then
    -    Result:=fPathNodeCache[Index].Node
    -  else
    +  begin
    +    Result:=fPathNodeCache[Index].Node;
    +    aNodeSearchName:=fPathNodeCache[Index].NodeSearchName;
    +  end else
    +  begin
         Result:=nil;
    +    aNodeSearchName:='';
    +  end;
     end;
     
    +function TXMLConfig.GetChildCount(const APath: string): Integer;
    +var
    +  Node: TDOMNode;
    +begin
    +  Node:=FindNode(APath,false);
    +  if Node=nil then
    +    Result := 0
    +  else
    +    Result := Node.GetChildCount;
    +end;
    +
     constructor TXMLConfig.Create(const AFilename: String);
     begin
       //DebugLn(['TXMLConfig.Create ',AFilename]);
    @@ -294,6 +422,41 @@
       Result:=StrToExtended(GetValue(APath,''),ADefault);
     end;
     
    +function TXMLConfig.GetListItemCount(const APath, AItemName: string;
    +  const aLegacyList: Boolean): Integer;
    +var
    +  Node: TDOMNode;
    +  NodeLevel: SizeInt;
    +begin
    +  if aLegacyList then
    +    Result := GetValue(APath+'Count',0)
    +  else
    +  begin
    +    Node:=InternalFindNode(APath,Length(APath));
    +    if Node<>nil then
    +    begin
    +      NodeLevel := Node.GetLevel-1;
    +      fPathNodeCache[NodeLevel].RefreshNodeListIfNeeded(AItemName);
    +      Result := Length(fPathNodeCache[NodeLevel].NodeList);
    +    end else
    +      Result := 0;
    +  end;
    +end;
    +
    +function TXMLConfig.GetListItemXPath(const AName: string;
    +  const AIndex: Integer; const aLegacyList: Boolean;
    +  const aLegacyList1Based: Boolean): string;
    +begin
    +  if ALegacyList then
    +  begin
    +    if aLegacyList1Based then
    +      Result := AName+IntToStr(AIndex+1)
    +    else
    +      Result := AName+IntToStr(AIndex);
    +  end else
    +    Result := AName+'['+IntToStr(AIndex+1)+']';
    +end;
    +
     procedure TXMLConfig.SetValue(const APath, AValue: String);
     var
       Node: TDOMNode;
    @@ -450,6 +613,11 @@
       InvalidateCacheTilEnd(0);
     end;
     
    +function TXMLConfig.IsLegacyList(const APath: string): Boolean;
    +begin
    +  Result := GetValue(APath+'Count',-1)>1;
    +end;
    +
     function TXMLConfig.ExtendedToStr(const e: extended): string;
     begin
       Result := FloatToStr(e, FPointSettings);
    @@ -478,8 +646,16 @@
       FreeAndNil(doc);
     end;
     
    -procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode);
    +function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
     var
    +  x: string;
    +begin
    +  Result := GetCachedPathNode(Index, x);
    +end;
    +
    +procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode;
    +  aNodeSearchName: string);
    +var
       OldLength, NewLength: Integer;
     begin
       OldLength:=length(fPathNodeCache);
    @@ -495,9 +671,13 @@
         exit
       else
         InvalidateCacheTilEnd(Index+1);
    +  if aNodeSearchName='' then
    +    aNodeSearchName:=aNode.NodeName;
       with fPathNodeCache[Index] do begin
         Node:=aNode;
    +    NodeSearchName:=aNodeSearchName;
         ChildrenValid:=false;
    +    NodeListName:='';
       end;
     end;
     
    @@ -510,6 +690,7 @@
           if Node=nil then break;
           Node:=nil;
           ChildrenValid:=false;
    +      NodeListName:='';
         end;
       end;
     end;
    @@ -517,11 +698,9 @@
     function TXMLConfig.InternalFindNode(const APath: String; PathLen: integer;
       CreateNodes: boolean): TDomNode;
     var
    -  NodePath: String;
    +  NodePath, NdName: String;
       StartPos, EndPos: integer;
       PathIndex: Integer;
    -  Parent: TDOMNode;
    -  NdName: DOMString;
       NameLen: Integer;
     begin
       //debugln(['TXMLConfig.InternalFindNode APath="',copy(APath,1,PathLen),'" CreateNodes=',CreateNodes]);
    @@ -539,25 +718,15 @@
         NameLen:=EndPos-StartPos;
         if NameLen=0 then break;
         inc(PathIndex);
    -    Parent:=Result;
    -    Result:=GetCachedPathNode(PathIndex);
    -    if Result<>nil then
    -      NdName:=Result.NodeName;
    +    Result:=GetCachedPathNode(PathIndex,NdName);
         if (Result=nil) or (length(NdName)<>NameLen)
         or not CompareMem(PChar(NdName),@APath[StartPos],NameLen) then begin
           // different path => search
           NodePath:=copy(APath,StartPos,NameLen);
    -      Result:=FindChildNode(PathIndex-1,NodePath);
    -      if Result=nil then begin
    -        if not CreateNodes then exit;
    -        // create missing node
    -        Result:=Doc.CreateElement(NodePath);
    -        Parent.AppendChild(Result);
    -        fPathNodeCache[PathIndex-1].ChildrenValid:=false;
    -        InvalidateCacheTilEnd(PathIndex);
    -        if EndPos>PathLen then exit;
    -      end;
    -      SetPathNodeCache(PathIndex,Result);
    +      Result:=FindChildNode(PathIndex-1,NodePath,CreateNodes);
    +      if Result=nil then
    +        Exit;
    +      SetPathNodeCache(PathIndex,Result,NodePath);
         end;
         StartPos:=EndPos+1;
         if StartPos>PathLen then exit;
    @@ -581,62 +750,56 @@
       end;
     end;
     
    -function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string
    -  ): TDomNode;
    +function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string;
    +  CreateNodes: boolean): TDomNode;
     var
    -  aParent, aChild: TDOMNode;
    -  aCount: Integer;
    -  NewLength: Integer;
       l, r, m: Integer;
    -  cmp: Integer;
    +  cmp, BrPos: Integer;
    +  NodeName: string;
     begin
    -  with fPathNodeCache[PathIndex] do begin
    -    if not ChildrenValid then begin
    -      // collect all children and sort
    -      aParent:=Node;
    -      aCount:=0;
    -      aChild:=aParent.FirstChild;
    -      while aChild<>nil do begin
    -        if aChild.NodeName<>'' then begin
    -          if aCount=length(Children) then begin
    -            NewLength:=length(Children);
    -            if NewLength<8 then
    -              NewLength:=8
    -            else
    -              NewLength:=NewLength*2;
    -            SetLength(Children,NewLength);
    -          end;
    -          Children[aCount]:=aChild;
    -          inc(aCount);
    -        end;
    -        aChild:=aChild.NextSibling;
    -      end;
    -      SetLength(Children,aCount);
    -      if aCount>1 then
    -        MergeSortWithLen(@Children[0],aCount,@CompareDomNodeNames); // sort ascending [0]<[1]
    -      for m:=0 to aCount-2 do
    -        if Children[m].NodeName=Children[m+1].NodeName then begin
    -          // duplicate found: nodes with same name
    -          // -> use only the first
    -          Children[m+1]:=Children[m];
    -        end;
    -      ChildrenValid:=true;
    +  BrPos := Pos('[', aName);
    +  if (Length(aName)>=BrPos+2) and (aName[Length(aName)]=']')
    +  and TryStrToInt(Trim(Copy(aName, BrPos+1, Length(aName)-BrPos-1)), m) then
    +  begin
    +    // support XPath in format "name[?]"
    +    NodeName := Trim(Copy(aName, 1, BrPos-1));
    +    fPathNodeCache[PathIndex].RefreshNodeListIfNeeded(NodeName);
    +    if m<=0 then
    +      raise Exception.CreateFmt('Invalid node index in XPath descriptor "%s".', [aName])
    +    else if (m<=Length(fPathNodeCache[PathIndex].NodeList)) then
    +      Result:=fPathNodeCache[PathIndex].NodeList[m-1]
    +    else if CreateNodes then
    +    begin
    +      for l := Length(fPathNodeCache[PathIndex].NodeList)+1 to m do
    +        Result := fPathNodeCache[PathIndex].AddNodeToList;
    +      InvalidateCacheTilEnd(PathIndex+1);
         end;
    +  end else
    +  begin
    +    fPathNodeCache[PathIndex].RefreshChildrenIfNeeded;
     
         // binary search
         l:=0;
    -    r:=length(Children)-1;
    +    r:=length(fPathNodeCache[PathIndex].Children)-1;
         while l<=r do begin
           m:=(l+r) shr 1;
    -      cmp:=CompareStr(aName,Children[m].NodeName);
    +      cmp:=CompareStr(aName,fPathNodeCache[PathIndex].Children[m].NodeName);
           if cmp<0 then
             r:=m-1
           else if cmp>0 then
             l:=m+1
           else
    -        exit(Children[m]);
    +        exit(fPathNodeCache[PathIndex].Children[m]);
         end;
    -    Result:=nil;
    +    if CreateNodes then
    +    begin
    +      // create missing node
    +      Result:=Doc.CreateElement(aName);
    +      fPathNodeCache[PathIndex].Node.AppendChild(Result);
    +      fPathNodeCache[PathIndex].ChildrenValid:=false;
    +      InvalidateCacheTilEnd(PathIndex+1);
    +    end else
    +      Result:=nil;
       end;
     end;
     
    @@ -686,6 +849,13 @@
       {$IFDEF MEM_CHECK}CheckHeapWrtMemCnt('TXMLConfig.SetFilename END');{$ENDIF}
     end;
     
    +procedure TXMLConfig.SetListItemCount(const APath: string;
    +  const ACount: Integer; const ALegacyList: Boolean);
    +begin
    +  if ALegacyList then
    +    SetDeleteValue(APath+'Count',ACount,0)
    +end;
    +
     procedure TXMLConfig.CreateConfigNode;
     var
       cfg: TDOMElement;
    Index: ide/frames/project_misc_options.lfm
    ===================================================================
    --- ide/frames/project_misc_options.lfm	(revision 60489)
    +++ ide/frames/project_misc_options.lfm	(working copy)
    @@ -118,13 +118,13 @@
       end
       object ResourceGroupBox: TGroupBox
         AnchorSideLeft.Control = Owner
    -    AnchorSideTop.Control = LRSInOutputDirCheckBox
    +    AnchorSideTop.Control = CompatibilityModeCheckBox
         AnchorSideTop.Side = asrBottom
         AnchorSideRight.Control = Owner
         AnchorSideRight.Side = asrBottom
         Left = 0
         Height = 81
    -    Top = 231
    +    Top = 256
         Width = 536
         Anchors = [akTop, akLeft, akRight]
         BorderSpacing.Top = 6
    @@ -139,7 +139,7 @@
         ChildSizing.ControlsPerLine = 1
         ClientHeight = 61
         ClientWidth = 532
    -    TabOrder = 9
    +    TabOrder = 10
         object UseFPCResourcesRadioButton: TRadioButton
           Left = 6
           Height = 25
    @@ -183,7 +183,7 @@
         AnchorSideTop.Side = asrCenter
         Left = 0
         Height = 15
    -    Top = 331
    +    Top = 356
         Width = 83
         Caption = 'PathDelimLabel'
         ParentColor = False
    @@ -196,7 +196,7 @@
         AnchorSideRight.Side = asrBottom
         Left = 0
         Height = 3
    -    Top = 318
    +    Top = 343
         Width = 536
         Anchors = [akTop, akLeft, akRight]
         BorderSpacing.Top = 6
    @@ -209,12 +209,12 @@
         AnchorSideRight.Side = asrBottom
         Left = 89
         Height = 23
    -    Top = 327
    +    Top = 352
         Width = 259
         BorderSpacing.Left = 6
         BorderSpacing.Top = 6
         ItemHeight = 15
    -    TabOrder = 10
    +    TabOrder = 11
         Text = 'PathDelimComboBox'
       end
       object MainUnitHasScaledStatementCheckBox: TCheckBox
    @@ -231,4 +231,18 @@
         ShowHint = True
         TabOrder = 4
       end
    +  object CompatibilityModeCheckBox: TCheckBox
    +    AnchorSideLeft.Control = Owner
    +    AnchorSideTop.Control = LRSInOutputDirCheckBox
    +    AnchorSideTop.Side = asrBottom
    +    Left = 0
    +    Height = 19
    +    Top = 231
    +    Width = 175
    +    BorderSpacing.Top = 6
    +    Caption = 'CompatibilityModeCheckBox'
    +    ParentShowHint = False
    +    ShowHint = True
    +    TabOrder = 9
    +  end
     end
    Index: ide/frames/project_misc_options.pas
    ===================================================================
    --- ide/frames/project_misc_options.pas	(revision 60489)
    +++ ide/frames/project_misc_options.pas	(working copy)
    @@ -25,6 +25,7 @@
         Bevel2: TBevel;
         LRSInOutputDirCheckBox: TCheckBox;
         MainUnitHasCreateFormStatementsCheckBox: TCheckBox;
    +    CompatibilityModeCheckBox: TCheckBox;
         MainUnitHasTitleStatementCheckBox: TCheckBox;
         MainUnitHasScaledStatementCheckBox: TCheckBox;
         MainUnitHasUsesSectionForAllUnitsCheckBox: TCheckBox;
    @@ -68,6 +69,8 @@
       MainUnitHasTitleStatementCheckBox.Hint := lisIdeMaintainsTheTitleInMainUnit;
       MainUnitHasScaledStatementCheckBox.Caption := lisMainUnitHasApplicationScaledStatement;
       MainUnitHasScaledStatementCheckBox.Hint := lisIdeMaintainsScaledInMainUnit;
    +  CompatibilityModeCheckBox.Caption := lisLPICompatibilityModeCheckBox;
    +  CompatibilityModeCheckBox.Hint := lisLPICompatibilityModeCheckBoxHint;
       RunnableCheckBox.Caption := lisProjectIsRunnable;
       RunnableCheckBox.Hint := lisProjectIsRunnableHint;
       UseDesignTimePkgsCheckBox.Caption := lisUseDesignTimePackages;
    @@ -96,6 +99,7 @@
         MainUnitHasCreateFormStatementsCheckBox.Checked := (pfMainUnitHasCreateFormStatements in Flags);
         MainUnitHasTitleStatementCheckBox.Checked := (pfMainUnitHasTitleStatement in Flags);
         MainUnitHasScaledStatementCheckBox.Checked := (pfMainUnitHasScaledStatement in Flags);
    +    CompatibilityModeCheckBox.Checked := (pfCompatibilityMode in Flags);
         RunnableCheckBox.Checked := (pfRunnable in Flags);
         UseDesignTimePkgsCheckBox.Checked := (pfUseDesignTimePackages in Flags);
         AlwaysBuildCheckBox.Checked := (pfAlwaysBuild in Flags);
    @@ -140,6 +144,8 @@
                      MainUnitHasTitleStatementCheckBox.Checked);
       SetProjectFlag(pfMainUnitHasScaledStatement,
                      MainUnitHasScaledStatementCheckBox.Checked);
    +  SetProjectFlag(pfCompatibilityMode,
    +                 CompatibilityModeCheckBox.Checked);
       SetProjectFlag(pfRunnable, RunnableCheckBox.Checked);
       SetProjectFlag(pfUseDesignTimePackages, UseDesignTimePkgsCheckBox.Checked);
       SetProjectFlag(pfAlwaysBuild, AlwaysBuildCheckBox.Checked);
    Index: ide/imexportcompileropts.pas
    ===================================================================
    --- ide/imexportcompileropts.pas	(revision 60489)
    +++ ide/imexportcompileropts.pas	(working copy)
    @@ -262,7 +262,7 @@
     begin
       Result := OpenXML(Filename);
       if Result <> mrOK then Exit;
    -  Project1.BuildModes.SaveProjOptsToXMLConfig(fXMLConfig, '', False);
    +  Project1.BuildModes.SaveProjOptsToXMLConfig(fXMLConfig, '', False, True);
       fXMLConfig.Flush;
       ShowMessageFmt(lisSuccessfullyExportedBuildModes, [Project1.BuildModes.Count, Filename]);
     end;
    Index: ide/lazarusidestrconsts.pas
    ===================================================================
    --- ide/lazarusidestrconsts.pas	(revision 60489)
    +++ ide/lazarusidestrconsts.pas	(working copy)
    @@ -2692,6 +2692,8 @@
       lisIdeMaintainsTheTitleInMainUnit = 'The IDE maintains the title in main unit.';
       lisMainUnitHasApplicationScaledStatement = 'Main unit has Application.Scaled statement';
       lisIdeMaintainsScaledInMainUnit = 'The IDE maintains Application.Scaled (Hi-DPI) in main unit.';
    +  lisLPICompatibilityModeCheckBox = 'Maximize compatibility of project files (LPI and LPS)';
    +  lisLPICompatibilityModeCheckBoxHint = 'Check this if you want to open your project in legacy (2.0 and older) Lazarus versions.';
       lisProjectIsRunnable = 'Project is runnable';
       lisProjectIsRunnableHint = 'Generates a binary executable which can be run.';
       lisUseDesignTimePackages = 'Use design time packages';
    Index: ide/project.pp
    ===================================================================
    --- ide/project.pp	(revision 60489)
    +++ ide/project.pp	(working copy)
    @@ -589,7 +589,7 @@
         procedure LoadFromXMLConfig(XMLConfig: TXMLConfig; const Path: string);
         procedure SaveMacroValuesAtOldPlace(XMLConfig: TXMLConfig; const Path: string);
         procedure SaveToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
    -                              IsDefault: Boolean; var Cnt: integer);
    +                              IsDefault, ALegacyList: Boolean; var Cnt: integer);
         function GetCaption: string; override;
         function GetIndex: integer; override;
       public
    @@ -655,9 +655,9 @@
         procedure LoadSessionFromXMLConfig(XMLConfig: TXMLConfig; const Path: string;
                                            LoadAllOptions: boolean);
         procedure SaveProjOptsToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
    -                                      SaveSession: boolean);
    +                                      SaveSession, ALegacyList: boolean);
         procedure SaveSessionOptsToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
    -                                         SaveSession: boolean);
    +                                         SaveSession, ALegacyList: boolean);
       public
         property Items[Index: integer]: TProjectBuildMode read GetItems; default;
         property ChangeStamp: integer read FChangeStamp;
    @@ -785,6 +785,7 @@
         function GetSourceDirectories: TFileReferenceList;
         function GetTargetFilename: string;
         function GetUnits(Index: integer): TUnitInfo;
    +    function GetUseLegacyLists: Boolean;
         function JumpHistoryCheckPosition(
                                     APosition:TProjectJumpHistoryPosition): boolean;
         function OnUnitFileBackup(const Filename: string): TModalResult;
    @@ -1058,6 +1059,7 @@
         property EnableI18NForLFM: boolean read FEnableI18NForLFM write SetEnableI18NForLFM;
         property I18NExcludedIdentifiers: TStrings read FI18NExcludedIdentifiers;
         property I18NExcludedOriginals: TStrings read FI18NExcludedOriginals;
    +    property UseLegacyLists: Boolean read GetUseLegacyLists;
         property ForceUpdatePoFiles: Boolean read FForceUpdatePoFiles write FForceUpdatePoFiles;
         property FirstAutoRevertLockedUnit: TUnitInfo read GetFirstAutoRevertLockedUnit;
         property FirstLoadedUnit: TUnitInfo read GetFirstLoadedUnit;
    @@ -1134,7 +1136,7 @@
     implementation
     
     const
    -  ProjectInfoFileVersion = 11;
    +  ProjectInfoFileVersion = 12;
       ProjOptionsPath = 'ProjectOptions/';
     
     
    @@ -2813,12 +2815,13 @@
       SubPath: String;
       NewUnitFilename: String;
       OldUnitInfo: TUnitInfo;
    -  MergeUnitInfo: Boolean;
    +  MergeUnitInfo, LegacyList: Boolean;
     begin
       {$IFDEF IDE_MEM_CHECK}CheckHeapWrtMemCnt('TProject.ReadProject D reading units');{$ENDIF}
    -  NewUnitCount:=FXMLConfig.GetValue(Path+'Units/Count',0);
    +  LegacyList:=(FFileVersion<=11) or FXMLConfig.IsLegacyList(Path+'Units/');
    +  NewUnitCount:=FXMLConfig.GetListItemCount(Path+'Units/', 'Unit', LegacyList);
       for i := 0 to NewUnitCount - 1 do begin
    -    SubPath:=Path+'Units/Unit'+IntToStr(i)+'/';
    +    SubPath:=Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, LegacyList)+'/';
         NewUnitFilename:=FXMLConfig.GetValue(SubPath+'Filename/Value','');
         OnLoadSaveFilename(NewUnitFilename,true);
         // load unit and add it
    @@ -2867,7 +2870,7 @@
     const
       Path = ProjOptionsPath;
     begin
    -  if (FFileVersion=0) and (FXMLConfig.GetValue(Path+'Units/Count',0)=0) then
    +  if (FFileVersion=0) and (FXMLConfig.GetListItemCount(Path+'Units/', 'Unit', UseLegacyLists)=0) then
         if IDEMessageDialog(lisStrangeLpiFile,
             Format(lisTheFileDoesNotLookLikeALpiFile, [ProjectInfoFile]),
             mtConfirmation,[mbIgnore,mbAbort])<>mrIgnore
    @@ -3149,10 +3152,10 @@
       for i:=0 to UnitCount-1 do
         if UnitMustBeSaved(Units[i],FProjectWriteFlags,SaveSession) then begin
           Units[i].SaveToXMLConfig(FXMLConfig,
    -        Path+'Units/Unit'+IntToStr(SaveUnitCount)+'/',True,SaveSession,fCurStorePathDelim);
    +        Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, UseLegacyLists)+'/',True,SaveSession,fCurStorePathDelim);
           inc(SaveUnitCount);
         end;
    -  FXMLConfig.SetDeleteValue(Path+'Units/Count',SaveUnitCount,0);
    +  FXMLConfig.SetListItemCount(Path+'Units/',SaveUnitCount,UseLegacyLists);
     end;
     
     procedure TProject.SaveOtherDefines(const Path: string);
    @@ -3197,6 +3200,7 @@
     var
       CurFlags: TProjectWriteFlags;
     begin
    +  FFileVersion:=ProjectInfoFileVersion;
       // format
       FXMLConfig.SetValue(Path+'Version/Value',ProjectInfoFileVersion);
       FXMLConfig.SetDeleteValue(Path+'PathDelim/Value',PathDelimSwitchToDelim[fCurStorePathDelim],'/');
    @@ -3232,7 +3236,7 @@
       // save custom data
       SaveStringToStringTree(FXMLConfig,CustomData,Path+'CustomData/');
       // Save the macro values and compiler options
    -  BuildModes.SaveProjOptsToXMLConfig(FXMLConfig, Path, FSaveSessionInLPI);
    +  BuildModes.SaveProjOptsToXMLConfig(FXMLConfig, Path, FSaveSessionInLPI, UseLegacyLists);
       BuildModes.SaveSharedMatrixOptions(Path);
       if FSaveSessionInLPI then
         BuildModes.SaveSessionData(Path);
    @@ -3287,6 +3291,7 @@
     const
       Path = 'ProjectSession/';
     begin
    +  FFileVersion:=ProjectInfoFileVersion;
       fCurStorePathDelim:=SessionStorePathDelim;
       FXMLConfig.SetDeleteValue(Path+'PathDelim/Value',
                               PathDelimSwitchToDelim[fCurStorePathDelim],'/');
    @@ -3293,7 +3298,7 @@
       FXMLConfig.SetValue(Path+'Version/Value',ProjectInfoFileVersion);
     
       // Save the session build modes
    -  BuildModes.SaveSessionOptsToXMLConfig(FXMLConfig, Path, True);
    +  BuildModes.SaveSessionOptsToXMLConfig(FXMLConfig, Path, True, UseLegacyLists);
       BuildModes.SaveSessionData(Path);
       // save all units
       SaveUnits(Path,true);
    @@ -4453,6 +4458,11 @@
       end;
     end;
     
    +function TProject.GetUseLegacyLists: Boolean;
    +begin
    +  Result := (FFileVersion<=11) or (pfCompatibilityMode in Flags);
    +end;
    +
     function TProject.HasProjectInfoFileChangedOnDisk: boolean;
     var
       AnUnitInfo: TUnitInfo;
    @@ -6814,13 +6824,13 @@
       XMLConfig.SetDeleteValue(Path+'Count',Cnt,0);
     end;
     
    -procedure TProjectBuildMode.SaveToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
    -  IsDefault: Boolean; var Cnt: integer);
    +procedure TProjectBuildMode.SaveToXMLConfig(XMLConfig: TXMLConfig;
    +  const Path: string; IsDefault, ALegacyList: Boolean; var Cnt: integer);
     var
       SubPath: String;
     begin
    +  SubPath:=Path+'BuildModes/'+XMLConfig.GetListItemXPath('Item', Cnt, ALegacyList, True)+'/';
       inc(Cnt);
    -  SubPath:=Path+'BuildModes/Item'+IntToStr(Cnt)+'/';
       XMLConfig.SetDeleteValue(SubPath+'Name',Identifier,'');
       if IsDefault then
         XMLConfig.SetDeleteValue(SubPath+'Default',True,false)
    @@ -7198,10 +7208,12 @@
       i: Integer;
       Ident, SubPath: String;
       CurMode: TProjectBuildMode;
    +  LegacyList: Boolean;
     begin
    +  LegacyList := FXMLConfig.IsLegacyList(Path);
       for i:=FromIndex to ToIndex do
       begin
    -    SubPath:=Path+'Item'+IntToStr(i)+'/';
    +    SubPath:=Path+FXMLConfig.GetListItemXPath('Item', i-1, LegacyList, True)+'/';
         Ident:=FXMLConfig.GetValue(SubPath+'Name','');
         CurMode:=Add(Ident);                     // add another mode
         CurMode.InSession:=InSession;
    @@ -7231,13 +7243,15 @@
     var
       i: Integer;
       SubPath: String;
    +  IsLegacyList: Boolean;
     begin
       // First default mode.
       LoadMacroValues(Path+'MacroValues/', Items[0]);
    +  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes/');
       // Iterate rest of the modes.
       for i:=2 to Cnt do
       begin
    -    SubPath:=Path+'BuildModes/Item'+IntToStr(i)+'/';
    +    SubPath:=Path+'BuildModes/'+FXMLConfig.GetListItemXPath('Item', i-1, IsLegacyList, True);
         LoadMacroValues(SubPath+'MacroValues/', Items[i-1]);
       end;
     end;
    @@ -7279,15 +7293,17 @@
     // Load for project
     var
       Cnt: Integer;
    +  IsLegacyList: Boolean;
     begin
       FXMLConfig := XMLConfig;
     
    -  Cnt:=FXMLConfig.GetValue(Path+'BuildModes/Count',0);
    +  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes/');
    +  Cnt:=FXMLConfig.GetListItemCount(Path+'BuildModes/', 'Item', IsLegacyList);
       if Cnt>0 then begin
         // Project default mode is stored at the old XML path for backward compatibility.
         // Testing the 'Default' XML attribute is not needed because the first mode
         // is always default.
    -    Items[0].Identifier:=FXMLConfig.GetValue(Path+'BuildModes/Item1/Name', '');
    +    Items[0].Identifier:=FXMLConfig.GetValue(Path+'BuildModes/'+XMLConfig.GetListItemXPath('Item', 0, IsLegacyList, True)+'/Name', '');
         Items[0].CompilerOptions.LoadFromXMLConfig(FXMLConfig, 'CompilerOptions/');
         LoadOtherCompilerOpts(Path+'BuildModes/', 2, Cnt, False);
         LoadAllMacroValues(Path+'MacroValues/', Cnt);
    @@ -7303,6 +7319,7 @@
     // Load for session
     var
       Cnt: Integer;
    +  IsLegacyList: Boolean;
     begin
       FXMLConfig := XMLConfig;
     
    @@ -7310,7 +7327,8 @@
         // load matrix options
         SessionMatrixOptions.LoadFromXMLConfig(FXMLConfig, Path+'BuildModes/SessionMatrixOptions/');
     
    -  Cnt:=FXMLConfig.GetValue(Path+'BuildModes/Count',0);
    +  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes/');
    +  Cnt:=FXMLConfig.GetListItemCount(Path+'BuildModes/', 'Item', IsLegacyList);
       if Cnt>0 then begin
         // Add a new mode for session compiler options.
         LoadOtherCompilerOpts(Path+'BuildModes/', 1, Cnt, True);
    @@ -7362,7 +7380,7 @@
     
     // SaveToXMLConfig itself
     procedure TProjectBuildModes.SaveProjOptsToXMLConfig(XMLConfig: TXMLConfig;
    -  const Path: string; SaveSession: boolean);
    +  const Path: string; SaveSession, ALegacyList: boolean);
     var
       i, Cnt: Integer;
     begin
    @@ -7377,12 +7395,12 @@
       Cnt:=0;
       for i:=0 to Count-1 do
         if SaveSession or not Items[i].InSession then
    -      Items[i].SaveToXMLConfig(FXMLConfig, Path, i=0, Cnt);
    -  FXMLConfig.SetDeleteValue(Path+'BuildModes/Count',Cnt,0);
    +      Items[i].SaveToXMLConfig(FXMLConfig, Path, i=0, ALegacyList, Cnt);
    +  FXMLConfig.SetListItemCount(Path+'BuildModes',Cnt,ALegacyList);
     end;
     
     procedure TProjectBuildModes.SaveSessionOptsToXMLConfig(XMLConfig: TXMLConfig;
    -  const Path: string; SaveSession: boolean);
    +  const Path: string; SaveSession, ALegacyList: boolean);
     var
       i, Cnt: Integer;
     begin
    @@ -7391,8 +7409,8 @@
       Cnt:=0;
       for i:=0 to Count-1 do
         if Items[i].InSession and SaveSession then
    -      Items[i].SaveToXMLConfig(FXMLConfig, Path, false, Cnt);
    -  FXMLConfig.SetDeleteValue(Path+'BuildModes/Count',Cnt,0);
    +      Items[i].SaveToXMLConfig(FXMLConfig, Path, false, ALegacyList, Cnt);
    +  FXMLConfig.SetListItemCount(Path+'BuildModes',Cnt,ALegacyList);
     end;
     
     
    

Relationships

related to 0035377 resolvedJuha Manninen Sometimes the IDE messes project info file up, causing newly added units not to show inside the Project Inspector. 
related to 0035267 resolvedJuha Manninen LPI: Enable CompatibilityMode when opening legacy projects, disable it for new projects 

Activities

Ondrej Pokorny

2019-02-24 20:23

developer  

xmlcfg-nodeindexes-01.patch (9,393 bytes)
Index: components/lazutils/laz2_xmlcfg.pas
===================================================================
--- components/lazutils/laz2_xmlcfg.pas	(revision 60489)
+++ components/lazutils/laz2_xmlcfg.pas	(working copy)
@@ -52,6 +52,7 @@
     type
       TNodeCache = record
         Node: TDomNode;
+        NodeSearchName: string;
         ChildrenValid: boolean;
         Children: array of TDomNode; // nodes with NodeName<>'' and sorted
       end;
@@ -68,13 +69,15 @@
     procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); virtual;
     procedure WriteXMLFile(ADoc: TXMLDocument; const AFileName: String); virtual;
     procedure FreeDoc; virtual;
-    procedure SetPathNodeCache(Index: integer; aNode: TDomNode);
+    procedure SetPathNodeCache(Index: integer; aNode: TDomNode; aNodeSearchName: string = '');
     function GetCachedPathNode(Index: integer): TDomNode; inline;
+    function GetCachedPathNode(Index: integer; out aNodeSearchName: string): TDomNode; inline;
     procedure InvalidateCacheTilEnd(StartIndex: integer);
     function InternalFindNode(const APath: String; PathLen: integer;
                               CreateNodes: boolean = false): TDomNode;
     procedure InternalCleanNode(Node: TDomNode);
-    function FindChildNode(PathIndex: integer; const aName: string): TDomNode;
+    function FindChildNode(PathIndex: integer; const aName: string;
+      CreateNodes: boolean = false): TDomNode;
   public
     constructor Create(AOwner: TComponent); override; overload;
     constructor Create(const AFilename: String); overload; // create and load
@@ -109,6 +112,7 @@
     // checks if the path has values, set PathHasValue=true to skip the last part
     function HasPath(const APath: string; PathHasValue: boolean): boolean;
     function HasChildPaths(const APath: string): boolean;
+    function GetChildCount(const APath: string): Integer;
     property Modified: Boolean read FModified write FModified;
     procedure InvalidatePathCache;
   published
@@ -151,14 +155,31 @@
 end;
 
 // inline
-function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
+function TXMLConfig.GetCachedPathNode(Index: integer; out
+  aNodeSearchName: string): TDomNode;
 begin
   if Index<length(fPathNodeCache) then
-    Result:=fPathNodeCache[Index].Node
-  else
+  begin
+    Result:=fPathNodeCache[Index].Node;
+    aNodeSearchName:=fPathNodeCache[Index].NodeSearchName;
+  end else
+  begin
     Result:=nil;
+    aNodeSearchName:='';
+  end;
 end;
 
+function TXMLConfig.GetChildCount(const APath: string): Integer;
+var
+  Node: TDOMNode;
+begin
+  Node:=FindNode(APath,false);
+  if Node=nil then
+    Result := 0
+  else
+    Result := Node.GetChildCount;
+end;
+
 constructor TXMLConfig.Create(const AFilename: String);
 begin
   //DebugLn(['TXMLConfig.Create ',AFilename]);
@@ -478,8 +499,16 @@
   FreeAndNil(doc);
 end;
 
-procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode);
+function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
 var
+  x: string;
+begin
+  Result := GetCachedPathNode(Index, x);
+end;
+
+procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode;
+  aNodeSearchName: string);
+var
   OldLength, NewLength: Integer;
 begin
   OldLength:=length(fPathNodeCache);
@@ -495,8 +524,11 @@
     exit
   else
     InvalidateCacheTilEnd(Index+1);
+  if aNodeSearchName='' then
+    aNodeSearchName:=aNode.NodeName;
   with fPathNodeCache[Index] do begin
     Node:=aNode;
+    NodeSearchName:=aNodeSearchName;
     ChildrenValid:=false;
   end;
 end;
@@ -517,11 +549,9 @@
 function TXMLConfig.InternalFindNode(const APath: String; PathLen: integer;
   CreateNodes: boolean): TDomNode;
 var
-  NodePath: String;
+  NodePath, NdName: String;
   StartPos, EndPos: integer;
   PathIndex: Integer;
-  Parent: TDOMNode;
-  NdName: DOMString;
   NameLen: Integer;
 begin
   //debugln(['TXMLConfig.InternalFindNode APath="',copy(APath,1,PathLen),'" CreateNodes=',CreateNodes]);
@@ -539,25 +569,15 @@
     NameLen:=EndPos-StartPos;
     if NameLen=0 then break;
     inc(PathIndex);
-    Parent:=Result;
-    Result:=GetCachedPathNode(PathIndex);
-    if Result<>nil then
-      NdName:=Result.NodeName;
+    Result:=GetCachedPathNode(PathIndex,NdName);
     if (Result=nil) or (length(NdName)<>NameLen)
     or not CompareMem(PChar(NdName),@APath[StartPos],NameLen) then begin
       // different path => search
       NodePath:=copy(APath,StartPos,NameLen);
-      Result:=FindChildNode(PathIndex-1,NodePath);
-      if Result=nil then begin
-        if not CreateNodes then exit;
-        // create missing node
-        Result:=Doc.CreateElement(NodePath);
-        Parent.AppendChild(Result);
-        fPathNodeCache[PathIndex-1].ChildrenValid:=false;
-        InvalidateCacheTilEnd(PathIndex);
-        if EndPos>PathLen then exit;
-      end;
-      SetPathNodeCache(PathIndex,Result);
+      Result:=FindChildNode(PathIndex-1,NodePath,CreateNodes);
+      if Result=nil then
+        Exit;
+      SetPathNodeCache(PathIndex,Result,NodePath);
     end;
     StartPos:=EndPos+1;
     if StartPos>PathLen then exit;
@@ -581,8 +601,9 @@
   end;
 end;
 
-function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string
-  ): TDomNode;
+function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string;
+  CreateNodes: boolean): TDomNode;
+
 var
   aParent, aChild: TDOMNode;
   aCount: Integer;
@@ -589,6 +610,8 @@
   NewLength: Integer;
   l, r, m: Integer;
   cmp: Integer;
+  BrPos: SizeInt;
+  NewName: string;
 begin
   with fPathNodeCache[PathIndex] do begin
     if not ChildrenValid then begin
@@ -623,20 +646,53 @@
       ChildrenValid:=true;
     end;
 
-    // binary search
-    l:=0;
-    r:=length(Children)-1;
-    while l<=r do begin
-      m:=(l+r) shr 1;
-      cmp:=CompareStr(aName,Children[m].NodeName);
-      if cmp<0 then
-        r:=m-1
-      else if cmp>0 then
-        l:=m+1
-      else
-        exit(Children[m]);
+    BrPos := Pos('[', aName);
+    if (Length(aName)>=BrPos+2) and (aName[Length(aName)]=']')
+    and TryStrToInt(Trim(Copy(aName, BrPos+1, Length(aName)-BrPos-1)), m) then
+    begin
+      // support XPath in format "*[?]" or "name[?]" - !!! the name is actually not checked - only the index
+      // the name is used only for creating child nodes
+      // do not use Children here because they are sorted and filtered
+      if m<=0 then
+        Result:=nil // error
+      else if (m<=Node.ChildNodes.Count) then
+        Result:=Node.ChildNodes[m-1]
+      else if CreateNodes then
+      begin
+        NewName := Trim(Copy(aName, 1, BrPos-1));
+        for m := Node.ChildNodes.Count+1 to m do
+        begin
+          Result:=Doc.CreateElement(NewName);
+          Node.AppendChild(Result);
+        end;
+        fPathNodeCache[PathIndex].ChildrenValid:=false;
+        InvalidateCacheTilEnd(PathIndex+1);
+      end;
+    end else
+    begin
+      // binary search
+      l:=0;
+      r:=length(Children)-1;
+      while l<=r do begin
+        m:=(l+r) shr 1;
+        cmp:=CompareStr(aName,Children[m].NodeName);
+        if cmp<0 then
+          r:=m-1
+        else if cmp>0 then
+          l:=m+1
+        else
+          exit(Children[m]);
+      end;
+      if CreateNodes then
+      begin
+        // create missing node
+        Result:=Doc.CreateElement(aName);
+        Node.AppendChild(Result);
+        fPathNodeCache[PathIndex].ChildrenValid:=false;
+        InvalidateCacheTilEnd(PathIndex+1);
+      end else
+        Result:=nil;
     end;
-    Result:=nil;
   end;
 end;
 
Index: ide/project.pp
===================================================================
--- ide/project.pp	(revision 60489)
+++ ide/project.pp	(working copy)
@@ -1134,7 +1134,7 @@
 implementation
 
 const
-  ProjectInfoFileVersion = 11;
+  ProjectInfoFileVersion = 12;
   ProjOptionsPath = 'ProjectOptions/';
 
 
@@ -2816,9 +2816,9 @@
   MergeUnitInfo: Boolean;
 begin
   {$IFDEF IDE_MEM_CHECK}CheckHeapWrtMemCnt('TProject.ReadProject D reading units');{$ENDIF}
-  NewUnitCount:=FXMLConfig.GetValue(Path+'Units/Count',0);
+  NewUnitCount:=FXMLConfig.GetChildCount(Path+'Units');
   for i := 0 to NewUnitCount - 1 do begin
-    SubPath:=Path+'Units/Unit'+IntToStr(i)+'/';
+    SubPath:=Path+'Units/Unit['+IntToStr(i+1)+']/';
     NewUnitFilename:=FXMLConfig.GetValue(SubPath+'Filename/Value','');
     OnLoadSaveFilename(NewUnitFilename,true);
     // load unit and add it
@@ -2867,7 +2867,7 @@
 const
   Path = ProjOptionsPath;
 begin
-  if (FFileVersion=0) and (FXMLConfig.GetValue(Path+'Units/Count',0)=0) then
+  if (FFileVersion=0) and (FXMLConfig.GetChildCount(Path+'Units')=0) then
     if IDEMessageDialog(lisStrangeLpiFile,
         Format(lisTheFileDoesNotLookLikeALpiFile, [ProjectInfoFile]),
         mtConfirmation,[mbIgnore,mbAbort])<>mrIgnore
@@ -3149,10 +3149,9 @@
   for i:=0 to UnitCount-1 do
     if UnitMustBeSaved(Units[i],FProjectWriteFlags,SaveSession) then begin
       Units[i].SaveToXMLConfig(FXMLConfig,
-        Path+'Units/Unit'+IntToStr(SaveUnitCount)+'/',True,SaveSession,fCurStorePathDelim);
+        Path+'Units/Unit['+IntToStr(SaveUnitCount+1)+']/',True,SaveSession,fCurStorePathDelim);
       inc(SaveUnitCount);
     end;
-  FXMLConfig.SetDeleteValue(Path+'Units/Count',SaveUnitCount,0);
 end;
 
 procedure TProject.SaveOtherDefines(const Path: string);

Ondrej Pokorny

2019-02-24 20:31

developer   ~0114385

I created and uploaded a first experimental patch - xmlcfg-nodeindexes-01.patch

DONE:
1.) Remove the field "count"
2.) Remove the unit number in the XML tag
3.) implement the reader such that it reads old-fashion-lpi files, too

NOT DONE:
A.) Forwards-compatibility for old Lazarus versions. (That means the ability to open project files from 2.1 in 2.0.) If this is wanted I assume we need some project or IDE option that determines if the new XML structure or the old XML structure should be written. IMO there is no chance that old Lazarus IDE reads the new XML structure. So if we want forwards-compatibility for Lazarus 2.0 and older, we must keep the ability to write the old structure.

B.) All similar counted structures in LPS, LPI etc. should be changed and use the item nodes without index. I can do so if I get a green light that this patch is going to be applied.

---

Please comment on the feature - AFAIR there was demand raised also in the mailing list, although I cannot find the thread any more.

Cyrax

2019-02-25 00:45

reporter   ~0114394

Oh yes, this is definitely needed feature.

I have had hard time to trying to merge changes on project files by hand. Usually I just revert them to original state and then apply my changes rather than trying to merging them manually. And this is time consuming.

Martok

2019-02-25 10:25

reporter   ~0114397

Another vote from here.

BuildModes and RequiredPackages are also written in this not-really-xml format.

I don't think forward-compatibility is needed. IIRC there was a way to notify the user that a project file would have to be updated, so it wouldn't come as a surprise?

errno

2019-02-25 11:54

reporter   ~0114399

I agree , Backward compatibility is enough .
Forward compatibility is in general never done , and just a source of problems .

Martin Friebe

2019-02-25 12:05

manager   ~0114400

This would be a new feature, so 2.2.
But if the reader is applied to 2.0.2 (and maybe the 1.8 branch) then I think that covers forward compatibility.

I have a look at the patch later.

Ondrej Pokorny

2019-02-25 12:38

developer   ~0114402

Thank you, Martin.

Let's see what Werner thinks about the forward compatibility. He has always been concerned about it.

wp

2019-02-25 13:44

developer   ~0114403

Last edited: 2019-02-25 13:45

View 2 revisions

Yes, I think this is important. In my components I try to support many versions, and it is often necessary to compile and compare with an old Lazarus, and even 1.8 branch is often not enough, some Linux distributions have even older versions, just look at the forum and see what people use.

What about (optionally?) writing two files: the old .lpi and a new (say) .lpx (x for xml)?

Or provide a "compatibility package" which can be installed into old Lazarus versions and converts the old format to the new one.

Ondrej Pokorny

2019-02-25 14:05

developer   ~0114406

If you want to have forward compatibility, the easiest way is to have an option in the project settings. It could be e.g. called "Maximize LPI and LPS file compatibility" - check it and it will write XML structure that can be read with legacy versions. With this you can decide what projects you need in the old LPI format and what in the new one.

I took inspiration from Photoshop: it has the same option. It's called "Maximize PSD and PSB File Compatibility.

> Or provide a "compatibility package" which can be installed into old Lazarus versions and converts the old format to the new one.

You probably meant "... and converts the new format to the old one". Do old Lazarus versions offer an interface for it?

wp

2019-02-25 14:26

developer   ~0114408

> Do old Lazarus versions offer an interface for it?

Oh, typing faster than thinking...

Martin Friebe

2019-02-25 16:53

manager   ~0114412

The issue with forward compatibility is, that if new Lazarus can/should write the old format on request, then it also needs the old "ProjectInfoFileVersion".

But that means, if the next feature increases the version, then the next feature must also be included in the option. I.e., if the old format is written with the old version, then all new features may have to been stripped from the output (features that only add new tags do not matter, but if they change existing tags then they become incompatible with the old version).

In that case it may be better to keep the version and have another indicator for the new format.
The xml config could write <... count="*" > or <... AutoCount="true"> and detect this when reading.

GetChildCount maybe should be GetChildCount(Patch, Match: String)
While the <units> section should only contain <unit> tags, if someone put anything else in there (maybe in future), then that should not be in the count.

It does not need to match unit1..unitN, because for those the automated count gets zero, and the "count" property must be used anyway. (reading old files, includes getting an error, if the count property is wrong).

Ondrej Pokorny

2019-02-25 18:30

developer   ~0114419

Yes, of course we could make an option to choose what "ProjectInfoFileVersion" to write. If you want 12, write the 12-format; if you want 13, write the 13-format etc.
There is just one problem: you want to write legacy list format so that you can test your project in legacy Lazarus versions but you want to store other new settings as well. No chance to do that.

> In that case it may be better to keep the version and have another indicator for the new format.

This is exactly what I meant with "Maximize LPI and LPS file compatibility" - of course this option must be written somewhere in the LPI file so that Lazarus knows to write the old format when you reopen the project.

> The xml config could write <... count="*" > or <... AutoCount="true"> and detect this when reading.
You still need the big "maximize compatibility" settings in the LPI, so the AutoCount at every list is a duplicate.

> While the <units> section should only contain <unit> tags, if someone put anything else in there (maybe in future), then that should not be in the count.

Well, with the new format you can put only <unit> tags into the <units>, you must not put anything else there. I believe this is currently the case for all XML configuration lists - the parent element consists only of <itemN> children and nothing else. Mattias may confirm that.

If we want be very strict, we should come up with an XSD that would describe the LPI format and this restriction :) (But AFAIK there is no way to create an XSD for the current <itemN> structure.)

-----

To sum up, IMO it is perfectly sufficient to have one boolean option in the LPI "CompatibilityFlag" (= "Maximize LPI and LPS file compatibility" in project options). The "CompatibilityFlag" in combination with "ProjectInfoFileVersion" will cover all cases elegantly.

Ondrej Pokorny

2019-02-26 08:28

developer  

xmlcfg-nodeindexes-02.patch (18,289 bytes)
Index: components/ideintf/projectintf.pas
===================================================================
--- components/ideintf/projectintf.pas	(revision 60489)
+++ components/ideintf/projectintf.pas	(working copy)
@@ -247,7 +247,8 @@
     pfLRSFilesInOutputDirectory, // put .lrs files in output directory
     pfUseDefaultCompilerOptions, // load users default compiler options
     pfSaveJumpHistory,
-    pfSaveFoldState
+    pfSaveFoldState,
+    pfCompatibilityMode // use legacy file format to maximize compatibility with old Lazarus versions
     );
   TProjectFlags = set of TProjectFlag;
 
@@ -274,7 +275,8 @@
       'LRSInOutputDirectory',
       'UseDefaultCompilerOptions',
       'SaveJumpHistory',
-      'SaveFoldState'
+      'SaveFoldState',
+      'CompatibilityMode'
     );
   ProjectSessionStorageNames: array[TProjectSessionStorage] of string = (
     'InProjectInfo',
Index: components/lazutils/laz2_xmlcfg.pas
===================================================================
--- components/lazutils/laz2_xmlcfg.pas	(revision 60489)
+++ components/lazutils/laz2_xmlcfg.pas	(working copy)
@@ -52,6 +52,7 @@
     type
       TNodeCache = record
         Node: TDomNode;
+        NodeSearchName: string;
         ChildrenValid: boolean;
         Children: array of TDomNode; // nodes with NodeName<>'' and sorted
       end;
@@ -68,13 +69,15 @@
     procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); virtual;
     procedure WriteXMLFile(ADoc: TXMLDocument; const AFileName: String); virtual;
     procedure FreeDoc; virtual;
-    procedure SetPathNodeCache(Index: integer; aNode: TDomNode);
+    procedure SetPathNodeCache(Index: integer; aNode: TDomNode; aNodeSearchName: string = '');
     function GetCachedPathNode(Index: integer): TDomNode; inline;
+    function GetCachedPathNode(Index: integer; out aNodeSearchName: string): TDomNode; inline;
     procedure InvalidateCacheTilEnd(StartIndex: integer);
     function InternalFindNode(const APath: String; PathLen: integer;
                               CreateNodes: boolean = false): TDomNode;
     procedure InternalCleanNode(Node: TDomNode);
-    function FindChildNode(PathIndex: integer; const aName: string): TDomNode;
+    function FindChildNode(PathIndex: integer; const aName: string;
+      CreateNodes: boolean = false): TDomNode;
   public
     constructor Create(AOwner: TComponent); override; overload;
     constructor Create(const AFilename: String); overload; // create and load
@@ -109,6 +112,10 @@
     // checks if the path has values, set PathHasValue=true to skip the last part
     function HasPath(const APath: string; PathHasValue: boolean): boolean;
     function HasChildPaths(const APath: string): boolean;
+    function GetChildCount(const APath: string): Integer;
+    function GetListItemCount(const APath: string; const ALegacyList: Boolean): Integer;
+    function GetListItemXPath(const AName: string; const AIndex: Integer; const ALegacyList: Boolean): string;
+    procedure SetListItemCount(const APath: string; const ACount: Integer; const ALegacyList: Boolean);
     property Modified: Boolean read FModified write FModified;
     procedure InvalidatePathCache;
   published
@@ -151,14 +158,31 @@
 end;
 
 // inline
-function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
+function TXMLConfig.GetCachedPathNode(Index: integer; out
+  aNodeSearchName: string): TDomNode;
 begin
   if Index<length(fPathNodeCache) then
-    Result:=fPathNodeCache[Index].Node
-  else
+  begin
+    Result:=fPathNodeCache[Index].Node;
+    aNodeSearchName:=fPathNodeCache[Index].NodeSearchName;
+  end else
+  begin
     Result:=nil;
+    aNodeSearchName:='';
+  end;
 end;
 
+function TXMLConfig.GetChildCount(const APath: string): Integer;
+var
+  Node: TDOMNode;
+begin
+  Node:=FindNode(APath,false);
+  if Node=nil then
+    Result := 0
+  else
+    Result := Node.GetChildCount;
+end;
+
 constructor TXMLConfig.Create(const AFilename: String);
 begin
   //DebugLn(['TXMLConfig.Create ',AFilename]);
@@ -294,6 +318,24 @@
   Result:=StrToExtended(GetValue(APath,''),ADefault);
 end;
 
+function TXMLConfig.GetListItemCount(const APath: string;
+  const ALegacyList: Boolean): Integer;
+begin
+  if ALegacyList then
+    Result := GetValue(APath+'Count',0)
+  else
+    Result := GetChildCount(APath);
+end;
+
+function TXMLConfig.GetListItemXPath(const AName: string;
+  const AIndex: Integer; const ALegacyList: Boolean): string;
+begin
+  if ALegacyList then
+    Result := AName+IntToStr(AIndex)
+  else
+    Result := AName+'['+IntToStr(AIndex+1)+']';
+end;
+
 procedure TXMLConfig.SetValue(const APath, AValue: String);
 var
   Node: TDOMNode;
@@ -478,8 +520,16 @@
   FreeAndNil(doc);
 end;
 
-procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode);
+function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
 var
+  x: string;
+begin
+  Result := GetCachedPathNode(Index, x);
+end;
+
+procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode;
+  aNodeSearchName: string);
+var
   OldLength, NewLength: Integer;
 begin
   OldLength:=length(fPathNodeCache);
@@ -495,8 +545,11 @@
     exit
   else
     InvalidateCacheTilEnd(Index+1);
+  if aNodeSearchName='' then
+    aNodeSearchName:=aNode.NodeName;
   with fPathNodeCache[Index] do begin
     Node:=aNode;
+    NodeSearchName:=aNodeSearchName;
     ChildrenValid:=false;
   end;
 end;
@@ -517,11 +570,9 @@
 function TXMLConfig.InternalFindNode(const APath: String; PathLen: integer;
   CreateNodes: boolean): TDomNode;
 var
-  NodePath: String;
+  NodePath, NdName: String;
   StartPos, EndPos: integer;
   PathIndex: Integer;
-  Parent: TDOMNode;
-  NdName: DOMString;
   NameLen: Integer;
 begin
   //debugln(['TXMLConfig.InternalFindNode APath="',copy(APath,1,PathLen),'" CreateNodes=',CreateNodes]);
@@ -539,25 +590,15 @@
     NameLen:=EndPos-StartPos;
     if NameLen=0 then break;
     inc(PathIndex);
-    Parent:=Result;
-    Result:=GetCachedPathNode(PathIndex);
-    if Result<>nil then
-      NdName:=Result.NodeName;
+    Result:=GetCachedPathNode(PathIndex,NdName);
     if (Result=nil) or (length(NdName)<>NameLen)
     or not CompareMem(PChar(NdName),@APath[StartPos],NameLen) then begin
       // different path => search
       NodePath:=copy(APath,StartPos,NameLen);
-      Result:=FindChildNode(PathIndex-1,NodePath);
-      if Result=nil then begin
-        if not CreateNodes then exit;
-        // create missing node
-        Result:=Doc.CreateElement(NodePath);
-        Parent.AppendChild(Result);
-        fPathNodeCache[PathIndex-1].ChildrenValid:=false;
-        InvalidateCacheTilEnd(PathIndex);
-        if EndPos>PathLen then exit;
-      end;
-      SetPathNodeCache(PathIndex,Result);
+      Result:=FindChildNode(PathIndex-1,NodePath,CreateNodes);
+      if Result=nil then
+        Exit;
+      SetPathNodeCache(PathIndex,Result,NodePath);
     end;
     StartPos:=EndPos+1;
     if StartPos>PathLen then exit;
@@ -581,8 +622,9 @@
   end;
 end;
 
-function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string
-  ): TDomNode;
+function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string;
+  CreateNodes: boolean): TDomNode;
+
 var
   aParent, aChild: TDOMNode;
   aCount: Integer;
@@ -589,6 +631,8 @@
   NewLength: Integer;
   l, r, m: Integer;
   cmp: Integer;
+  BrPos: SizeInt;
+  NewName: string;
 begin
   with fPathNodeCache[PathIndex] do begin
     if not ChildrenValid then begin
@@ -623,20 +667,53 @@
       ChildrenValid:=true;
     end;
 
-    // binary search
-    l:=0;
-    r:=length(Children)-1;
-    while l<=r do begin
-      m:=(l+r) shr 1;
-      cmp:=CompareStr(aName,Children[m].NodeName);
-      if cmp<0 then
-        r:=m-1
-      else if cmp>0 then
-        l:=m+1
-      else
-        exit(Children[m]);
+    BrPos := Pos('[', aName);
+    if (Length(aName)>=BrPos+2) and (aName[Length(aName)]=']')
+    and TryStrToInt(Trim(Copy(aName, BrPos+1, Length(aName)-BrPos-1)), m) then
+    begin
+      // support XPath in format "*[?]" or "name[?]" - !!! the name is actually not checked - only the index
+      // the name is used only for creating child nodes
+      // do not use Children here because they are sorted and filtered
+      if m<=0 then
+        Result:=nil // error
+      else if (m<=Node.ChildNodes.Count) then
+        Result:=Node.ChildNodes[m-1]
+      else if CreateNodes then
+      begin
+        NewName := Trim(Copy(aName, 1, BrPos-1));
+        for m := Node.ChildNodes.Count+1 to m do
+        begin
+          Result:=Doc.CreateElement(NewName);
+          Node.AppendChild(Result);
+        end;
+        fPathNodeCache[PathIndex].ChildrenValid:=false;
+        InvalidateCacheTilEnd(PathIndex+1);
+      end;
+    end else
+    begin
+      // binary search
+      l:=0;
+      r:=length(Children)-1;
+      while l<=r do begin
+        m:=(l+r) shr 1;
+        cmp:=CompareStr(aName,Children[m].NodeName);
+        if cmp<0 then
+          r:=m-1
+        else if cmp>0 then
+          l:=m+1
+        else
+          exit(Children[m]);
+      end;
+      if CreateNodes then
+      begin
+        // create missing node
+        Result:=Doc.CreateElement(aName);
+        Node.AppendChild(Result);
+        fPathNodeCache[PathIndex].ChildrenValid:=false;
+        InvalidateCacheTilEnd(PathIndex+1);
+      end else
+        Result:=nil;
     end;
-    Result:=nil;
   end;
 end;
 
@@ -686,6 +763,13 @@
   {$IFDEF MEM_CHECK}CheckHeapWrtMemCnt('TXMLConfig.SetFilename END');{$ENDIF}
 end;
 
+procedure TXMLConfig.SetListItemCount(const APath: string;
+  const ACount: Integer; const ALegacyList: Boolean);
+begin
+  if ALegacyList then
+    SetDeleteValue(APath+'Count',ACount,0)
+end;
+
 procedure TXMLConfig.CreateConfigNode;
 var
   cfg: TDOMElement;
Index: ide/frames/project_misc_options.lfm
===================================================================
--- ide/frames/project_misc_options.lfm	(revision 60489)
+++ ide/frames/project_misc_options.lfm	(working copy)
@@ -118,13 +118,13 @@
   end
   object ResourceGroupBox: TGroupBox
     AnchorSideLeft.Control = Owner
-    AnchorSideTop.Control = LRSInOutputDirCheckBox
+    AnchorSideTop.Control = CompatibilityModeCheckBox
     AnchorSideTop.Side = asrBottom
     AnchorSideRight.Control = Owner
     AnchorSideRight.Side = asrBottom
     Left = 0
     Height = 81
-    Top = 231
+    Top = 256
     Width = 536
     Anchors = [akTop, akLeft, akRight]
     BorderSpacing.Top = 6
@@ -139,7 +139,7 @@
     ChildSizing.ControlsPerLine = 1
     ClientHeight = 61
     ClientWidth = 532
-    TabOrder = 9
+    TabOrder = 10
     object UseFPCResourcesRadioButton: TRadioButton
       Left = 6
       Height = 25
@@ -183,7 +183,7 @@
     AnchorSideTop.Side = asrCenter
     Left = 0
     Height = 15
-    Top = 331
+    Top = 356
     Width = 83
     Caption = 'PathDelimLabel'
     ParentColor = False
@@ -196,7 +196,7 @@
     AnchorSideRight.Side = asrBottom
     Left = 0
     Height = 3
-    Top = 318
+    Top = 343
     Width = 536
     Anchors = [akTop, akLeft, akRight]
     BorderSpacing.Top = 6
@@ -209,12 +209,12 @@
     AnchorSideRight.Side = asrBottom
     Left = 89
     Height = 23
-    Top = 327
+    Top = 352
     Width = 259
     BorderSpacing.Left = 6
     BorderSpacing.Top = 6
     ItemHeight = 15
-    TabOrder = 10
+    TabOrder = 11
     Text = 'PathDelimComboBox'
   end
   object MainUnitHasScaledStatementCheckBox: TCheckBox
@@ -231,4 +231,18 @@
     ShowHint = True
     TabOrder = 4
   end
+  object CompatibilityModeCheckBox: TCheckBox
+    AnchorSideLeft.Control = Owner
+    AnchorSideTop.Control = LRSInOutputDirCheckBox
+    AnchorSideTop.Side = asrBottom
+    Left = 0
+    Height = 19
+    Top = 231
+    Width = 175
+    BorderSpacing.Top = 6
+    Caption = 'CompatibilityModeCheckBox'
+    ParentShowHint = False
+    ShowHint = True
+    TabOrder = 9
+  end
 end
Index: ide/frames/project_misc_options.pas
===================================================================
--- ide/frames/project_misc_options.pas	(revision 60489)
+++ ide/frames/project_misc_options.pas	(working copy)
@@ -25,6 +25,7 @@
     Bevel2: TBevel;
     LRSInOutputDirCheckBox: TCheckBox;
     MainUnitHasCreateFormStatementsCheckBox: TCheckBox;
+    CompatibilityModeCheckBox: TCheckBox;
     MainUnitHasTitleStatementCheckBox: TCheckBox;
     MainUnitHasScaledStatementCheckBox: TCheckBox;
     MainUnitHasUsesSectionForAllUnitsCheckBox: TCheckBox;
@@ -68,6 +69,8 @@
   MainUnitHasTitleStatementCheckBox.Hint := lisIdeMaintainsTheTitleInMainUnit;
   MainUnitHasScaledStatementCheckBox.Caption := lisMainUnitHasApplicationScaledStatement;
   MainUnitHasScaledStatementCheckBox.Hint := lisIdeMaintainsScaledInMainUnit;
+  CompatibilityModeCheckBox.Caption := lisLPICompatibilityModeCheckBox;
+  CompatibilityModeCheckBox.Hint := lisLPICompatibilityModeCheckBoxHint;
   RunnableCheckBox.Caption := lisProjectIsRunnable;
   RunnableCheckBox.Hint := lisProjectIsRunnableHint;
   UseDesignTimePkgsCheckBox.Caption := lisUseDesignTimePackages;
@@ -96,6 +99,7 @@
     MainUnitHasCreateFormStatementsCheckBox.Checked := (pfMainUnitHasCreateFormStatements in Flags);
     MainUnitHasTitleStatementCheckBox.Checked := (pfMainUnitHasTitleStatement in Flags);
     MainUnitHasScaledStatementCheckBox.Checked := (pfMainUnitHasScaledStatement in Flags);
+    CompatibilityModeCheckBox.Checked := (pfCompatibilityMode in Flags);
     RunnableCheckBox.Checked := (pfRunnable in Flags);
     UseDesignTimePkgsCheckBox.Checked := (pfUseDesignTimePackages in Flags);
     AlwaysBuildCheckBox.Checked := (pfAlwaysBuild in Flags);
@@ -140,6 +144,8 @@
                  MainUnitHasTitleStatementCheckBox.Checked);
   SetProjectFlag(pfMainUnitHasScaledStatement,
                  MainUnitHasScaledStatementCheckBox.Checked);
+  SetProjectFlag(pfCompatibilityMode,
+                 CompatibilityModeCheckBox.Checked);
   SetProjectFlag(pfRunnable, RunnableCheckBox.Checked);
   SetProjectFlag(pfUseDesignTimePackages, UseDesignTimePkgsCheckBox.Checked);
   SetProjectFlag(pfAlwaysBuild, AlwaysBuildCheckBox.Checked);
Index: ide/lazarusidestrconsts.pas
===================================================================
--- ide/lazarusidestrconsts.pas	(revision 60489)
+++ ide/lazarusidestrconsts.pas	(working copy)
@@ -2692,6 +2692,8 @@
   lisIdeMaintainsTheTitleInMainUnit = 'The IDE maintains the title in main unit.';
   lisMainUnitHasApplicationScaledStatement = 'Main unit has Application.Scaled statement';
   lisIdeMaintainsScaledInMainUnit = 'The IDE maintains Application.Scaled (Hi-DPI) in main unit.';
+  lisLPICompatibilityModeCheckBox = 'Maximize compatibility of project files (LPI and LPS)';
+  lisLPICompatibilityModeCheckBoxHint = 'Check this if you want to open your project in legacy (2.0 and older) Lazarus versions.';
   lisProjectIsRunnable = 'Project is runnable';
   lisProjectIsRunnableHint = 'Generates a binary executable which can be run.';
   lisUseDesignTimePackages = 'Use design time packages';
Index: ide/project.pp
===================================================================
--- ide/project.pp	(revision 60489)
+++ ide/project.pp	(working copy)
@@ -785,6 +785,7 @@
     function GetSourceDirectories: TFileReferenceList;
     function GetTargetFilename: string;
     function GetUnits(Index: integer): TUnitInfo;
+    function GetUseLegacyLists: Boolean;
     function JumpHistoryCheckPosition(
                                 APosition:TProjectJumpHistoryPosition): boolean;
     function OnUnitFileBackup(const Filename: string): TModalResult;
@@ -1058,6 +1059,7 @@
     property EnableI18NForLFM: boolean read FEnableI18NForLFM write SetEnableI18NForLFM;
     property I18NExcludedIdentifiers: TStrings read FI18NExcludedIdentifiers;
     property I18NExcludedOriginals: TStrings read FI18NExcludedOriginals;
+    property UseLegacyLists: Boolean read GetUseLegacyLists;
     property ForceUpdatePoFiles: Boolean read FForceUpdatePoFiles write FForceUpdatePoFiles;
     property FirstAutoRevertLockedUnit: TUnitInfo read GetFirstAutoRevertLockedUnit;
     property FirstLoadedUnit: TUnitInfo read GetFirstLoadedUnit;
@@ -1134,7 +1136,7 @@
 implementation
 
 const
-  ProjectInfoFileVersion = 11;
+  ProjectInfoFileVersion = 12;
   ProjOptionsPath = 'ProjectOptions/';
 
 
@@ -2816,9 +2818,9 @@
   MergeUnitInfo: Boolean;
 begin
   {$IFDEF IDE_MEM_CHECK}CheckHeapWrtMemCnt('TProject.ReadProject D reading units');{$ENDIF}
-  NewUnitCount:=FXMLConfig.GetValue(Path+'Units/Count',0);
+  NewUnitCount:=FXMLConfig.GetListItemCount(Path+'Units/', UseLegacyLists);
   for i := 0 to NewUnitCount - 1 do begin
-    SubPath:=Path+'Units/Unit'+IntToStr(i)+'/';
+    SubPath:=Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, UseLegacyLists)+'/';
     NewUnitFilename:=FXMLConfig.GetValue(SubPath+'Filename/Value','');
     OnLoadSaveFilename(NewUnitFilename,true);
     // load unit and add it
@@ -2867,7 +2869,7 @@
 const
   Path = ProjOptionsPath;
 begin
-  if (FFileVersion=0) and (FXMLConfig.GetValue(Path+'Units/Count',0)=0) then
+  if (FFileVersion=0) and (FXMLConfig.GetListItemCount(Path+'Units/', UseLegacyLists)=0) then
     if IDEMessageDialog(lisStrangeLpiFile,
         Format(lisTheFileDoesNotLookLikeALpiFile, [ProjectInfoFile]),
         mtConfirmation,[mbIgnore,mbAbort])<>mrIgnore
@@ -3149,10 +3151,10 @@
   for i:=0 to UnitCount-1 do
     if UnitMustBeSaved(Units[i],FProjectWriteFlags,SaveSession) then begin
       Units[i].SaveToXMLConfig(FXMLConfig,
-        Path+'Units/Unit'+IntToStr(SaveUnitCount)+'/',True,SaveSession,fCurStorePathDelim);
+        Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, UseLegacyLists)+'/',True,SaveSession,fCurStorePathDelim);
       inc(SaveUnitCount);
     end;
-  FXMLConfig.SetDeleteValue(Path+'Units/Count',SaveUnitCount,0);
+  FXMLConfig.SetListItemCount(Path+'Units/',SaveUnitCount,UseLegacyLists);
 end;
 
 procedure TProject.SaveOtherDefines(const Path: string);
@@ -4453,6 +4455,11 @@
   end;
 end;
 
+function TProject.GetUseLegacyLists: Boolean;
+begin
+  Result := (FFileVersion<=11) or (pfCompatibilityMode in Flags);
+end;
+
 function TProject.HasProjectInfoFileChangedOnDisk: boolean;
 var
   AnUnitInfo: TUnitInfo;

Ondrej Pokorny

2019-02-26 08:32

developer   ~0114459

02.patch: a proof of concept with the CompatibilityMode project flag.

You can choose to write CompatibilityMode LPI/LPS in Project Options -> Miscellaneous

DONE:
A.) Forwards-compatibility for old Lazarus versions.

Still NOT DONE:
B.) All similar counted structures in LPS and LPI.

Martin Friebe

2019-02-26 15:19

manager   ~0114463

"you can put only <unit> tags into the <units>, you must not put anything else there"

Yes, but it could easily happen that in future we want to put other tags in there.

If today we implement count according to that, then we can do so in future without breaking anything.

Martok

2019-02-26 19:40

reporter   ~0114468

Huh? That's not how the DOM works...

Unless something in the XPath engine is broken, this will correctly work regardless of what else is in the node. It doesn't matter what other nodes are there to resolve //Units/Unit[5] to the fifth [Unit] element. And the count is XPath('//Units/Unit').AsNodeSet.Count...

Speaking of which, querying the NodeSet once and indexing it would be faster than re-querying for every item, but that likely only works with the new, actually XML-compliant, format.

Martin Friebe

2019-02-26 20:37

manager   ~0114471

>> " And the count is XPath('//Units/Unit').AsNodeSet.Count."
Sorry, in that case I misread the patch. Will check again

Ondrej Pokorny

2019-02-27 07:44

developer   ~0114481

Last edited: 2019-02-27 07:45

View 2 revisions

> Huh? That's not how the DOM works...

Yes, but this is not about DOM, it is about the TXMLConfig - and currently it doesn't use the XPath engine at all - on the contrary it is written very differently indeed to achieve good performance in questioning absolute paths.

Unfortunately, the way TXMLConfig is written doesn't allow a "normal" XPath approach (find an element and use it). This is also a reason why now the strange <itemN> nodes are used. Just check yourself the code of TXMLConfig.

If you want to use the "normal" XPath approach as you suggested with reasonable speed, a huge rewrite of TXMLConfig and all the objects/methods that use TXMLConfig is necessary. I don't want to do that.

Nevertheless, Martok, feel free to modify my patch if you have an idea how to make things easier.

> "you can put only <unit> tags into the <units>, you must not put anything else there"

OK, this restriction is not really necessary.

Ondrej Pokorny

2019-02-27 12:57

developer   ~0114487

03.patch:

> "you can put only <unit> tags into the <units>, you must not put anything else there"

I removed this restriction.

Actually, I was wrong. Also currently there are node lists with foreign elements. One example is the BuildModes-element in LPI. It has <Item1>, <Item2>... and also SharedMatrixOptions and other child elements.

My patch can now cope with it and I demonstrated it by changing the BuildModes to the new list format.

Btw. I also had to take care of the fact that legacy lists can be 0- or 1-based: E.g. "Units" are 0-based and "BuildModes" are 1-based.

---

Martok, you can still rewrite my patch if you know how to achieve it more easily and efficiently - I have no problem with that.

Ondrej Pokorny

2019-02-27 12:58

developer  

xmlcfg-nodeindexes-03.patch (29,839 bytes)
Index: components/ideintf/projectintf.pas
===================================================================
--- components/ideintf/projectintf.pas	(revision 60489)
+++ components/ideintf/projectintf.pas	(working copy)
@@ -247,7 +247,8 @@
     pfLRSFilesInOutputDirectory, // put .lrs files in output directory
     pfUseDefaultCompilerOptions, // load users default compiler options
     pfSaveJumpHistory,
-    pfSaveFoldState
+    pfSaveFoldState,
+    pfCompatibilityMode // use legacy file format to maximize compatibility with old Lazarus versions
     );
   TProjectFlags = set of TProjectFlag;
 
@@ -274,7 +275,8 @@
       'LRSInOutputDirectory',
       'UseDefaultCompilerOptions',
       'SaveJumpHistory',
-      'SaveFoldState'
+      'SaveFoldState',
+      'CompatibilityMode'
     );
   ProjectSessionStorageNames: array[TProjectSessionStorage] of string = (
     'InProjectInfo',
Index: components/lazutils/laz2_xmlcfg.pas
===================================================================
--- components/lazutils/laz2_xmlcfg.pas	(revision 60489)
+++ components/lazutils/laz2_xmlcfg.pas	(working copy)
@@ -16,6 +16,7 @@
 }
 
 {$MODE objfpc}
+{$modeswitch advancedrecords}
 {$H+}
 
 unit Laz2_XMLCfg;
@@ -50,10 +51,23 @@
     procedure SetFilename(const AFilename: String);
   protected
     type
+      TDomNodeArray = array of TDomNode;
       TNodeCache = record
         Node: TDomNode;
+        NodeSearchName: string;
         ChildrenValid: boolean;
-        Children: array of TDomNode; // nodes with NodeName<>'' and sorted
+        Children: TDomNodeArray; // child nodes with NodeName<>'' and sorted
+
+        NodeListName: string;
+        NodeList: TDomNodeArray; // child nodes that are accessed with "name[?]" XPath
+
+      public
+        class procedure GrowArray(var aArray: TDomNodeArray; aCount: Integer); static;
+        procedure RefreshChildren;
+        procedure RefreshChildrenIfNeeded;
+        procedure RefreshNodeList(const ANodeName: string);
+        procedure RefreshNodeListIfNeeded(const ANodeName: string);
+        function AddNodeToList: TDOMNode;
       end;
   protected
     doc: TXMLDocument;
@@ -68,13 +82,15 @@
     procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); virtual;
     procedure WriteXMLFile(ADoc: TXMLDocument; const AFileName: String); virtual;
     procedure FreeDoc; virtual;
-    procedure SetPathNodeCache(Index: integer; aNode: TDomNode);
+    procedure SetPathNodeCache(Index: integer; aNode: TDomNode; aNodeSearchName: string = '');
     function GetCachedPathNode(Index: integer): TDomNode; inline;
+    function GetCachedPathNode(Index: integer; out aNodeSearchName: string): TDomNode; inline;
     procedure InvalidateCacheTilEnd(StartIndex: integer);
     function InternalFindNode(const APath: String; PathLen: integer;
                               CreateNodes: boolean = false): TDomNode;
     procedure InternalCleanNode(Node: TDomNode);
-    function FindChildNode(PathIndex: integer; const aName: string): TDomNode;
+    function FindChildNode(PathIndex: integer; const aName: string;
+      CreateNodes: boolean = false): TDomNode;
   public
     constructor Create(AOwner: TComponent); override; overload;
     constructor Create(const AFilename: String); overload; // create and load
@@ -109,6 +125,12 @@
     // checks if the path has values, set PathHasValue=true to skip the last part
     function HasPath(const APath: string; PathHasValue: boolean): boolean;
     function HasChildPaths(const APath: string): boolean;
+    function GetChildCount(const APath: string): Integer;
+    function IsLegacyList(const APath: string): Boolean;
+    function GetListItemCount(const APath, AItemName: string; const aLegacyList: Boolean): Integer;
+    function GetListItemXPath(const AName: string; const AIndex: Integer; const aLegacyList: Boolean;
+      const aLegacyList1Based: Boolean = False): string;
+    procedure SetListItemCount(const APath: string; const ACount: Integer; const ALegacyList: Boolean);
     property Modified: Boolean read FModified write FModified;
     procedure InvalidatePathCache;
   published
@@ -150,15 +172,121 @@
   Result:=CompareStr(Node1.NodeName,Node2.NodeName);
 end;
 
+{ TXMLConfig.TNodeCache }
+
+function TXMLConfig.TNodeCache.AddNodeToList: TDOMNode;
+begin
+  Result:=Node.OwnerDocument.CreateElement(NodeListName);
+  Node.AppendChild(Result);
+  SetLength(NodeList, Length(NodeList)+1);
+  NodeList[High(NodeList)]:=Result;
+end;
+
+class procedure TXMLConfig.TNodeCache.GrowArray(var aArray: TDomNodeArray;
+  aCount: Integer);
+var
+  cCount: Integer;
+begin
+  cCount:=length(aArray);
+  if aCount>cCount then begin
+    if cCount<8 then
+      cCount:=8
+    else
+      cCount:=cCount*2;
+    if aCount>cCount then
+      cCount := aCount;
+    SetLength(aArray,cCount);
+  end;
+end;
+
+procedure TXMLConfig.TNodeCache.RefreshChildren;
+var
+  aCount, m: Integer;
+  aChild: TDOMNode;
+begin
+  // collect all children and sort
+  aCount:=0;
+  aChild:=Node.FirstChild;
+  while aChild<>nil do begin
+    if aChild.NodeName<>'' then begin
+      GrowArray(Children, aCount+1);
+      Children[aCount]:=aChild;
+      inc(aCount);
+    end;
+    aChild:=aChild.NextSibling;
+  end;
+  SetLength(Children,aCount);
+  if aCount>1 then
+    MergeSortWithLen(@Children[0],aCount,@CompareDomNodeNames); // sort ascending [0]<[1]
+  for m:=0 to aCount-2 do
+    if Children[m].NodeName=Children[m+1].NodeName then begin
+      // duplicate found: nodes with same name
+      // -> use only the first
+      Children[m+1]:=Children[m];
+    end;
+  ChildrenValid:=true;
+end;
+
+procedure TXMLConfig.TNodeCache.RefreshChildrenIfNeeded;
+begin
+  if not ChildrenValid then
+    RefreshChildren;
+end;
+
+procedure TXMLConfig.TNodeCache.RefreshNodeList(const ANodeName: string);
+var
+  aCount: Integer;
+  aChild: TDOMNode;
+begin
+  aCount:=0;
+  aChild:=Node.FirstChild;
+  while aChild<>nil do
+  begin
+    if aChild.NodeName=ANodeName then
+    begin
+      GrowArray(NodeList, aCount+1);
+      NodeList[aCount]:=aChild;
+      inc(aCount);
+    end;
+    aChild:=aChild.NextSibling;
+  end;
+  SetLength(NodeList,aCount);
+  NodeListName := ANodeName;
+end;
+
+procedure TXMLConfig.TNodeCache.RefreshNodeListIfNeeded(const ANodeName: string
+  );
+begin
+  if NodeListName<>ANodeName then
+    RefreshNodeList(ANodeName);
+end;
+
 // inline
-function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
+function TXMLConfig.GetCachedPathNode(Index: integer; out
+  aNodeSearchName: string): TDomNode;
 begin
   if Index<length(fPathNodeCache) then
-    Result:=fPathNodeCache[Index].Node
-  else
+  begin
+    Result:=fPathNodeCache[Index].Node;
+    aNodeSearchName:=fPathNodeCache[Index].NodeSearchName;
+  end else
+  begin
     Result:=nil;
+    aNodeSearchName:='';
+  end;
 end;
 
+function TXMLConfig.GetChildCount(const APath: string): Integer;
+var
+  Node: TDOMNode;
+begin
+  Node:=FindNode(APath,false);
+  if Node=nil then
+    Result := 0
+  else
+    Result := Node.GetChildCount;
+end;
+
 constructor TXMLConfig.Create(const AFilename: String);
 begin
   //DebugLn(['TXMLConfig.Create ',AFilename]);
@@ -294,6 +422,41 @@
   Result:=StrToExtended(GetValue(APath,''),ADefault);
 end;
 
+function TXMLConfig.GetListItemCount(const APath, AItemName: string;
+  const aLegacyList: Boolean): Integer;
+var
+  Node: TDOMNode;
+  NodeLevel: SizeInt;
+begin
+  if aLegacyList then
+    Result := GetValue(APath+'Count',0)
+  else
+  begin
+    Node:=InternalFindNode(APath,Length(APath));
+    if Node<>nil then
+    begin
+      NodeLevel := Node.GetLevel-1;
+      fPathNodeCache[NodeLevel].RefreshNodeListIfNeeded(AItemName);
+      Result := Length(fPathNodeCache[NodeLevel].NodeList);
+    end else
+      Result := 0;
+  end;
+end;
+
+function TXMLConfig.GetListItemXPath(const AName: string;
+  const AIndex: Integer; const aLegacyList: Boolean;
+  const aLegacyList1Based: Boolean): string;
+begin
+  if ALegacyList then
+  begin
+    if aLegacyList1Based then
+      Result := AName+IntToStr(AIndex+1)
+    else
+      Result := AName+IntToStr(AIndex);
+  end else
+    Result := AName+'['+IntToStr(AIndex+1)+']';
+end;
+
 procedure TXMLConfig.SetValue(const APath, AValue: String);
 var
   Node: TDOMNode;
@@ -450,6 +613,11 @@
   InvalidateCacheTilEnd(0);
 end;
 
+function TXMLConfig.IsLegacyList(const APath: string): Boolean;
+begin
+  Result := GetValue(APath+'Count',-1)>1;
+end;
+
 function TXMLConfig.ExtendedToStr(const e: extended): string;
 begin
   Result := FloatToStr(e, FPointSettings);
@@ -478,8 +646,16 @@
   FreeAndNil(doc);
 end;
 
-procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode);
+function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
 var
+  x: string;
+begin
+  Result := GetCachedPathNode(Index, x);
+end;
+
+procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode;
+  aNodeSearchName: string);
+var
   OldLength, NewLength: Integer;
 begin
   OldLength:=length(fPathNodeCache);
@@ -495,9 +671,13 @@
     exit
   else
     InvalidateCacheTilEnd(Index+1);
+  if aNodeSearchName='' then
+    aNodeSearchName:=aNode.NodeName;
   with fPathNodeCache[Index] do begin
     Node:=aNode;
+    NodeSearchName:=aNodeSearchName;
     ChildrenValid:=false;
+    NodeListName:='';
   end;
 end;
 
@@ -510,6 +690,7 @@
       if Node=nil then break;
       Node:=nil;
       ChildrenValid:=false;
+      NodeListName:='';
     end;
   end;
 end;
@@ -517,11 +698,9 @@
 function TXMLConfig.InternalFindNode(const APath: String; PathLen: integer;
   CreateNodes: boolean): TDomNode;
 var
-  NodePath: String;
+  NodePath, NdName: String;
   StartPos, EndPos: integer;
   PathIndex: Integer;
-  Parent: TDOMNode;
-  NdName: DOMString;
   NameLen: Integer;
 begin
   //debugln(['TXMLConfig.InternalFindNode APath="',copy(APath,1,PathLen),'" CreateNodes=',CreateNodes]);
@@ -539,25 +718,15 @@
     NameLen:=EndPos-StartPos;
     if NameLen=0 then break;
     inc(PathIndex);
-    Parent:=Result;
-    Result:=GetCachedPathNode(PathIndex);
-    if Result<>nil then
-      NdName:=Result.NodeName;
+    Result:=GetCachedPathNode(PathIndex,NdName);
     if (Result=nil) or (length(NdName)<>NameLen)
     or not CompareMem(PChar(NdName),@APath[StartPos],NameLen) then begin
       // different path => search
       NodePath:=copy(APath,StartPos,NameLen);
-      Result:=FindChildNode(PathIndex-1,NodePath);
-      if Result=nil then begin
-        if not CreateNodes then exit;
-        // create missing node
-        Result:=Doc.CreateElement(NodePath);
-        Parent.AppendChild(Result);
-        fPathNodeCache[PathIndex-1].ChildrenValid:=false;
-        InvalidateCacheTilEnd(PathIndex);
-        if EndPos>PathLen then exit;
-      end;
-      SetPathNodeCache(PathIndex,Result);
+      Result:=FindChildNode(PathIndex-1,NodePath,CreateNodes);
+      if Result=nil then
+        Exit;
+      SetPathNodeCache(PathIndex,Result,NodePath);
     end;
     StartPos:=EndPos+1;
     if StartPos>PathLen then exit;
@@ -581,62 +750,56 @@
   end;
 end;
 
-function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string
-  ): TDomNode;
+function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string;
+  CreateNodes: boolean): TDomNode;
 var
-  aParent, aChild: TDOMNode;
-  aCount: Integer;
-  NewLength: Integer;
   l, r, m: Integer;
-  cmp: Integer;
+  cmp, BrPos: Integer;
+  NodeName: string;
 begin
-  with fPathNodeCache[PathIndex] do begin
-    if not ChildrenValid then begin
-      // collect all children and sort
-      aParent:=Node;
-      aCount:=0;
-      aChild:=aParent.FirstChild;
-      while aChild<>nil do begin
-        if aChild.NodeName<>'' then begin
-          if aCount=length(Children) then begin
-            NewLength:=length(Children);
-            if NewLength<8 then
-              NewLength:=8
-            else
-              NewLength:=NewLength*2;
-            SetLength(Children,NewLength);
-          end;
-          Children[aCount]:=aChild;
-          inc(aCount);
-        end;
-        aChild:=aChild.NextSibling;
-      end;
-      SetLength(Children,aCount);
-      if aCount>1 then
-        MergeSortWithLen(@Children[0],aCount,@CompareDomNodeNames); // sort ascending [0]<[1]
-      for m:=0 to aCount-2 do
-        if Children[m].NodeName=Children[m+1].NodeName then begin
-          // duplicate found: nodes with same name
-          // -> use only the first
-          Children[m+1]:=Children[m];
-        end;
-      ChildrenValid:=true;
+  BrPos := Pos('[', aName);
+  if (Length(aName)>=BrPos+2) and (aName[Length(aName)]=']')
+  and TryStrToInt(Trim(Copy(aName, BrPos+1, Length(aName)-BrPos-1)), m) then
+  begin
+    // support XPath in format "name[?]"
+    NodeName := Trim(Copy(aName, 1, BrPos-1));
+    fPathNodeCache[PathIndex].RefreshNodeListIfNeeded(NodeName);
+    if m<=0 then
+      raise Exception.CreateFmt('Invalid node index in XPath descriptor "%s".', [aName])
+    else if (m<=Length(fPathNodeCache[PathIndex].NodeList)) then
+      Result:=fPathNodeCache[PathIndex].NodeList[m-1]
+    else if CreateNodes then
+    begin
+      for l := Length(fPathNodeCache[PathIndex].NodeList)+1 to m do
+        Result := fPathNodeCache[PathIndex].AddNodeToList;
+      InvalidateCacheTilEnd(PathIndex+1);
     end;
+  end else
+  begin
+    fPathNodeCache[PathIndex].RefreshChildrenIfNeeded;
 
     // binary search
     l:=0;
-    r:=length(Children)-1;
+    r:=length(fPathNodeCache[PathIndex].Children)-1;
     while l<=r do begin
       m:=(l+r) shr 1;
-      cmp:=CompareStr(aName,Children[m].NodeName);
+      cmp:=CompareStr(aName,fPathNodeCache[PathIndex].Children[m].NodeName);
       if cmp<0 then
         r:=m-1
       else if cmp>0 then
         l:=m+1
       else
-        exit(Children[m]);
+        exit(fPathNodeCache[PathIndex].Children[m]);
     end;
-    Result:=nil;
+    if CreateNodes then
+    begin
+      // create missing node
+      Result:=Doc.CreateElement(aName);
+      fPathNodeCache[PathIndex].Node.AppendChild(Result);
+      fPathNodeCache[PathIndex].ChildrenValid:=false;
+      InvalidateCacheTilEnd(PathIndex+1);
+    end else
+      Result:=nil;
   end;
 end;
 
@@ -686,6 +849,13 @@
   {$IFDEF MEM_CHECK}CheckHeapWrtMemCnt('TXMLConfig.SetFilename END');{$ENDIF}
 end;
 
+procedure TXMLConfig.SetListItemCount(const APath: string;
+  const ACount: Integer; const ALegacyList: Boolean);
+begin
+  if ALegacyList then
+    SetDeleteValue(APath+'Count',ACount,0)
+end;
+
 procedure TXMLConfig.CreateConfigNode;
 var
   cfg: TDOMElement;
Index: ide/frames/project_misc_options.lfm
===================================================================
--- ide/frames/project_misc_options.lfm	(revision 60489)
+++ ide/frames/project_misc_options.lfm	(working copy)
@@ -118,13 +118,13 @@
   end
   object ResourceGroupBox: TGroupBox
     AnchorSideLeft.Control = Owner
-    AnchorSideTop.Control = LRSInOutputDirCheckBox
+    AnchorSideTop.Control = CompatibilityModeCheckBox
     AnchorSideTop.Side = asrBottom
     AnchorSideRight.Control = Owner
     AnchorSideRight.Side = asrBottom
     Left = 0
     Height = 81
-    Top = 231
+    Top = 256
     Width = 536
     Anchors = [akTop, akLeft, akRight]
     BorderSpacing.Top = 6
@@ -139,7 +139,7 @@
     ChildSizing.ControlsPerLine = 1
     ClientHeight = 61
     ClientWidth = 532
-    TabOrder = 9
+    TabOrder = 10
     object UseFPCResourcesRadioButton: TRadioButton
       Left = 6
       Height = 25
@@ -183,7 +183,7 @@
     AnchorSideTop.Side = asrCenter
     Left = 0
     Height = 15
-    Top = 331
+    Top = 356
     Width = 83
     Caption = 'PathDelimLabel'
     ParentColor = False
@@ -196,7 +196,7 @@
     AnchorSideRight.Side = asrBottom
     Left = 0
     Height = 3
-    Top = 318
+    Top = 343
     Width = 536
     Anchors = [akTop, akLeft, akRight]
     BorderSpacing.Top = 6
@@ -209,12 +209,12 @@
     AnchorSideRight.Side = asrBottom
     Left = 89
     Height = 23
-    Top = 327
+    Top = 352
     Width = 259
     BorderSpacing.Left = 6
     BorderSpacing.Top = 6
     ItemHeight = 15
-    TabOrder = 10
+    TabOrder = 11
     Text = 'PathDelimComboBox'
   end
   object MainUnitHasScaledStatementCheckBox: TCheckBox
@@ -231,4 +231,18 @@
     ShowHint = True
     TabOrder = 4
   end
+  object CompatibilityModeCheckBox: TCheckBox
+    AnchorSideLeft.Control = Owner
+    AnchorSideTop.Control = LRSInOutputDirCheckBox
+    AnchorSideTop.Side = asrBottom
+    Left = 0
+    Height = 19
+    Top = 231
+    Width = 175
+    BorderSpacing.Top = 6
+    Caption = 'CompatibilityModeCheckBox'
+    ParentShowHint = False
+    ShowHint = True
+    TabOrder = 9
+  end
 end
Index: ide/frames/project_misc_options.pas
===================================================================
--- ide/frames/project_misc_options.pas	(revision 60489)
+++ ide/frames/project_misc_options.pas	(working copy)
@@ -25,6 +25,7 @@
     Bevel2: TBevel;
     LRSInOutputDirCheckBox: TCheckBox;
     MainUnitHasCreateFormStatementsCheckBox: TCheckBox;
+    CompatibilityModeCheckBox: TCheckBox;
     MainUnitHasTitleStatementCheckBox: TCheckBox;
     MainUnitHasScaledStatementCheckBox: TCheckBox;
     MainUnitHasUsesSectionForAllUnitsCheckBox: TCheckBox;
@@ -68,6 +69,8 @@
   MainUnitHasTitleStatementCheckBox.Hint := lisIdeMaintainsTheTitleInMainUnit;
   MainUnitHasScaledStatementCheckBox.Caption := lisMainUnitHasApplicationScaledStatement;
   MainUnitHasScaledStatementCheckBox.Hint := lisIdeMaintainsScaledInMainUnit;
+  CompatibilityModeCheckBox.Caption := lisLPICompatibilityModeCheckBox;
+  CompatibilityModeCheckBox.Hint := lisLPICompatibilityModeCheckBoxHint;
   RunnableCheckBox.Caption := lisProjectIsRunnable;
   RunnableCheckBox.Hint := lisProjectIsRunnableHint;
   UseDesignTimePkgsCheckBox.Caption := lisUseDesignTimePackages;
@@ -96,6 +99,7 @@
     MainUnitHasCreateFormStatementsCheckBox.Checked := (pfMainUnitHasCreateFormStatements in Flags);
     MainUnitHasTitleStatementCheckBox.Checked := (pfMainUnitHasTitleStatement in Flags);
     MainUnitHasScaledStatementCheckBox.Checked := (pfMainUnitHasScaledStatement in Flags);
+    CompatibilityModeCheckBox.Checked := (pfCompatibilityMode in Flags);
     RunnableCheckBox.Checked := (pfRunnable in Flags);
     UseDesignTimePkgsCheckBox.Checked := (pfUseDesignTimePackages in Flags);
     AlwaysBuildCheckBox.Checked := (pfAlwaysBuild in Flags);
@@ -140,6 +144,8 @@
                  MainUnitHasTitleStatementCheckBox.Checked);
   SetProjectFlag(pfMainUnitHasScaledStatement,
                  MainUnitHasScaledStatementCheckBox.Checked);
+  SetProjectFlag(pfCompatibilityMode,
+                 CompatibilityModeCheckBox.Checked);
   SetProjectFlag(pfRunnable, RunnableCheckBox.Checked);
   SetProjectFlag(pfUseDesignTimePackages, UseDesignTimePkgsCheckBox.Checked);
   SetProjectFlag(pfAlwaysBuild, AlwaysBuildCheckBox.Checked);
Index: ide/imexportcompileropts.pas
===================================================================
--- ide/imexportcompileropts.pas	(revision 60489)
+++ ide/imexportcompileropts.pas	(working copy)
@@ -262,7 +262,7 @@
 begin
   Result := OpenXML(Filename);
   if Result <> mrOK then Exit;
-  Project1.BuildModes.SaveProjOptsToXMLConfig(fXMLConfig, '', False);
+  Project1.BuildModes.SaveProjOptsToXMLConfig(fXMLConfig, '', False, True);
   fXMLConfig.Flush;
   ShowMessageFmt(lisSuccessfullyExportedBuildModes, [Project1.BuildModes.Count, Filename]);
 end;
Index: ide/lazarusidestrconsts.pas
===================================================================
--- ide/lazarusidestrconsts.pas	(revision 60489)
+++ ide/lazarusidestrconsts.pas	(working copy)
@@ -2692,6 +2692,8 @@
   lisIdeMaintainsTheTitleInMainUnit = 'The IDE maintains the title in main unit.';
   lisMainUnitHasApplicationScaledStatement = 'Main unit has Application.Scaled statement';
   lisIdeMaintainsScaledInMainUnit = 'The IDE maintains Application.Scaled (Hi-DPI) in main unit.';
+  lisLPICompatibilityModeCheckBox = 'Maximize compatibility of project files (LPI and LPS)';
+  lisLPICompatibilityModeCheckBoxHint = 'Check this if you want to open your project in legacy (2.0 and older) Lazarus versions.';
   lisProjectIsRunnable = 'Project is runnable';
   lisProjectIsRunnableHint = 'Generates a binary executable which can be run.';
   lisUseDesignTimePackages = 'Use design time packages';
Index: ide/project.pp
===================================================================
--- ide/project.pp	(revision 60489)
+++ ide/project.pp	(working copy)
@@ -589,7 +589,7 @@
     procedure LoadFromXMLConfig(XMLConfig: TXMLConfig; const Path: string);
     procedure SaveMacroValuesAtOldPlace(XMLConfig: TXMLConfig; const Path: string);
     procedure SaveToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
-                              IsDefault: Boolean; var Cnt: integer);
+                              IsDefault, ALegacyList: Boolean; var Cnt: integer);
     function GetCaption: string; override;
     function GetIndex: integer; override;
   public
@@ -655,9 +655,9 @@
     procedure LoadSessionFromXMLConfig(XMLConfig: TXMLConfig; const Path: string;
                                        LoadAllOptions: boolean);
     procedure SaveProjOptsToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
-                                      SaveSession: boolean);
+                                      SaveSession, ALegacyList: boolean);
     procedure SaveSessionOptsToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
-                                         SaveSession: boolean);
+                                         SaveSession, ALegacyList: boolean);
   public
     property Items[Index: integer]: TProjectBuildMode read GetItems; default;
     property ChangeStamp: integer read FChangeStamp;
@@ -785,6 +785,7 @@
     function GetSourceDirectories: TFileReferenceList;
     function GetTargetFilename: string;
     function GetUnits(Index: integer): TUnitInfo;
+    function GetUseLegacyLists: Boolean;
     function JumpHistoryCheckPosition(
                                 APosition:TProjectJumpHistoryPosition): boolean;
     function OnUnitFileBackup(const Filename: string): TModalResult;
@@ -1058,6 +1059,7 @@
     property EnableI18NForLFM: boolean read FEnableI18NForLFM write SetEnableI18NForLFM;
     property I18NExcludedIdentifiers: TStrings read FI18NExcludedIdentifiers;
     property I18NExcludedOriginals: TStrings read FI18NExcludedOriginals;
+    property UseLegacyLists: Boolean read GetUseLegacyLists;
     property ForceUpdatePoFiles: Boolean read FForceUpdatePoFiles write FForceUpdatePoFiles;
     property FirstAutoRevertLockedUnit: TUnitInfo read GetFirstAutoRevertLockedUnit;
     property FirstLoadedUnit: TUnitInfo read GetFirstLoadedUnit;
@@ -1134,7 +1136,7 @@
 implementation
 
 const
-  ProjectInfoFileVersion = 11;
+  ProjectInfoFileVersion = 12;
   ProjOptionsPath = 'ProjectOptions/';
 
 
@@ -2813,12 +2815,13 @@
   SubPath: String;
   NewUnitFilename: String;
   OldUnitInfo: TUnitInfo;
-  MergeUnitInfo: Boolean;
+  MergeUnitInfo, LegacyList: Boolean;
 begin
   {$IFDEF IDE_MEM_CHECK}CheckHeapWrtMemCnt('TProject.ReadProject D reading units');{$ENDIF}
-  NewUnitCount:=FXMLConfig.GetValue(Path+'Units/Count',0);
+  LegacyList:=(FFileVersion<=11) or FXMLConfig.IsLegacyList(Path+'Units/');
+  NewUnitCount:=FXMLConfig.GetListItemCount(Path+'Units/', 'Unit', LegacyList);
   for i := 0 to NewUnitCount - 1 do begin
-    SubPath:=Path+'Units/Unit'+IntToStr(i)+'/';
+    SubPath:=Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, LegacyList)+'/';
     NewUnitFilename:=FXMLConfig.GetValue(SubPath+'Filename/Value','');
     OnLoadSaveFilename(NewUnitFilename,true);
     // load unit and add it
@@ -2867,7 +2870,7 @@
 const
   Path = ProjOptionsPath;
 begin
-  if (FFileVersion=0) and (FXMLConfig.GetValue(Path+'Units/Count',0)=0) then
+  if (FFileVersion=0) and (FXMLConfig.GetListItemCount(Path+'Units/', 'Unit', UseLegacyLists)=0) then
     if IDEMessageDialog(lisStrangeLpiFile,
         Format(lisTheFileDoesNotLookLikeALpiFile, [ProjectInfoFile]),
         mtConfirmation,[mbIgnore,mbAbort])<>mrIgnore
@@ -3149,10 +3152,10 @@
   for i:=0 to UnitCount-1 do
     if UnitMustBeSaved(Units[i],FProjectWriteFlags,SaveSession) then begin
       Units[i].SaveToXMLConfig(FXMLConfig,
-        Path+'Units/Unit'+IntToStr(SaveUnitCount)+'/',True,SaveSession,fCurStorePathDelim);
+        Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, UseLegacyLists)+'/',True,SaveSession,fCurStorePathDelim);
       inc(SaveUnitCount);
     end;
-  FXMLConfig.SetDeleteValue(Path+'Units/Count',SaveUnitCount,0);
+  FXMLConfig.SetListItemCount(Path+'Units/',SaveUnitCount,UseLegacyLists);
 end;
 
 procedure TProject.SaveOtherDefines(const Path: string);
@@ -3232,7 +3235,7 @@
   // save custom data
   SaveStringToStringTree(FXMLConfig,CustomData,Path+'CustomData/');
   // Save the macro values and compiler options
-  BuildModes.SaveProjOptsToXMLConfig(FXMLConfig, Path, FSaveSessionInLPI);
+  BuildModes.SaveProjOptsToXMLConfig(FXMLConfig, Path, FSaveSessionInLPI, UseLegacyLists);
   BuildModes.SaveSharedMatrixOptions(Path);
   if FSaveSessionInLPI then
     BuildModes.SaveSessionData(Path);
@@ -3293,7 +3296,7 @@
   FXMLConfig.SetValue(Path+'Version/Value',ProjectInfoFileVersion);
 
   // Save the session build modes
-  BuildModes.SaveSessionOptsToXMLConfig(FXMLConfig, Path, True);
+  BuildModes.SaveSessionOptsToXMLConfig(FXMLConfig, Path, True, UseLegacyLists);
   BuildModes.SaveSessionData(Path);
   // save all units
   SaveUnits(Path,true);
@@ -4453,6 +4456,11 @@
   end;
 end;
 
+function TProject.GetUseLegacyLists: Boolean;
+begin
+  Result := (FFileVersion<=11) or (pfCompatibilityMode in Flags);
+end;
+
 function TProject.HasProjectInfoFileChangedOnDisk: boolean;
 var
   AnUnitInfo: TUnitInfo;
@@ -6814,13 +6822,13 @@
   XMLConfig.SetDeleteValue(Path+'Count',Cnt,0);
 end;
 
-procedure TProjectBuildMode.SaveToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
-  IsDefault: Boolean; var Cnt: integer);
+procedure TProjectBuildMode.SaveToXMLConfig(XMLConfig: TXMLConfig;
+  const Path: string; IsDefault, ALegacyList: Boolean; var Cnt: integer);
 var
   SubPath: String;
 begin
+  SubPath:=Path+'BuildModes/'+XMLConfig.GetListItemXPath('Item', Cnt, ALegacyList, True)+'/';
   inc(Cnt);
-  SubPath:=Path+'BuildModes/Item'+IntToStr(Cnt)+'/';
   XMLConfig.SetDeleteValue(SubPath+'Name',Identifier,'');
   if IsDefault then
     XMLConfig.SetDeleteValue(SubPath+'Default',True,false)
@@ -7198,10 +7206,12 @@
   i: Integer;
   Ident, SubPath: String;
   CurMode: TProjectBuildMode;
+  LegacyList: Boolean;
 begin
+  LegacyList := FXMLConfig.IsLegacyList(Path);
   for i:=FromIndex to ToIndex do
   begin
-    SubPath:=Path+'Item'+IntToStr(i)+'/';
+    SubPath:=Path+FXMLConfig.GetListItemXPath('Item', i-1, LegacyList, True)+'/';
     Ident:=FXMLConfig.GetValue(SubPath+'Name','');
     CurMode:=Add(Ident);                     // add another mode
     CurMode.InSession:=InSession;
@@ -7231,13 +7241,15 @@
 var
   i: Integer;
   SubPath: String;
+  IsLegacyList: Boolean;
 begin
   // First default mode.
   LoadMacroValues(Path+'MacroValues/', Items[0]);
+  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes');
   // Iterate rest of the modes.
   for i:=2 to Cnt do
   begin
-    SubPath:=Path+'BuildModes/Item'+IntToStr(i)+'/';
+    SubPath:=Path+'BuildModes/'+FXMLConfig.GetListItemXPath('Item', i-1, IsLegacyList, True);
     LoadMacroValues(SubPath+'MacroValues/', Items[i-1]);
   end;
 end;
@@ -7279,15 +7291,17 @@
 // Load for project
 var
   Cnt: Integer;
+  IsLegacyList: Boolean;
 begin
   FXMLConfig := XMLConfig;
 
-  Cnt:=FXMLConfig.GetValue(Path+'BuildModes/Count',0);
+  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes');
+  Cnt:=FXMLConfig.GetListItemCount(Path+'BuildModes/', 'Item', IsLegacyList);
   if Cnt>0 then begin
     // Project default mode is stored at the old XML path for backward compatibility.
     // Testing the 'Default' XML attribute is not needed because the first mode
     // is always default.
-    Items[0].Identifier:=FXMLConfig.GetValue(Path+'BuildModes/Item1/Name', '');
+    Items[0].Identifier:=FXMLConfig.GetValue(Path+'BuildModes/'+XMLConfig.GetListItemXPath('Item', 0, IsLegacyList, True)+'/Name', '');
     Items[0].CompilerOptions.LoadFromXMLConfig(FXMLConfig, 'CompilerOptions/');
     LoadOtherCompilerOpts(Path+'BuildModes/', 2, Cnt, False);
     LoadAllMacroValues(Path+'MacroValues/', Cnt);
@@ -7362,7 +7376,7 @@
 
 // SaveToXMLConfig itself
 procedure TProjectBuildModes.SaveProjOptsToXMLConfig(XMLConfig: TXMLConfig;
-  const Path: string; SaveSession: boolean);
+  const Path: string; SaveSession, ALegacyList: boolean);
 var
   i, Cnt: Integer;
 begin
@@ -7377,12 +7391,12 @@
   Cnt:=0;
   for i:=0 to Count-1 do
     if SaveSession or not Items[i].InSession then
-      Items[i].SaveToXMLConfig(FXMLConfig, Path, i=0, Cnt);
-  FXMLConfig.SetDeleteValue(Path+'BuildModes/Count',Cnt,0);
+      Items[i].SaveToXMLConfig(FXMLConfig, Path, i=0, ALegacyList, Cnt);
+  FXMLConfig.SetListItemCount(Path+'BuildModes',Cnt,ALegacyList);
 end;
 
 procedure TProjectBuildModes.SaveSessionOptsToXMLConfig(XMLConfig: TXMLConfig;
-  const Path: string; SaveSession: boolean);
+  const Path: string; SaveSession, ALegacyList: boolean);
 var
   i, Cnt: Integer;
 begin
@@ -7391,8 +7405,8 @@
   Cnt:=0;
   for i:=0 to Count-1 do
     if Items[i].InSession and SaveSession then
-      Items[i].SaveToXMLConfig(FXMLConfig, Path, false, Cnt);
-  FXMLConfig.SetDeleteValue(Path+'BuildModes/Count',Cnt,0);
+      Items[i].SaveToXMLConfig(FXMLConfig, Path, false, ALegacyList, Cnt);
+  FXMLConfig.SetListItemCount(Path+'BuildModes',Cnt,ALegacyList);
 end;
 
 

Martok

2019-02-27 17:07

reporter   ~0114491

Oh, I thought from all the mentions of XPath in names etc. that the XPath engine was used. Silly me ;-)

Well, if TXMLConfig doesn't use the one thing that XML got right, that makes it more difficult...

Juha Manninen

2019-03-01 09:13

developer   ~0114534

This will be an important change. I also have bumbed into similar merge problems.
I tested by adding one unit (codehelp.pas) to LazBuild project. The ProjectOptions tag in lazbuild.lpi has now some empty <Unit/> tags :

  <Units>
    <Unit>
      <Filename Value="lazbuild.lpr"/>
      <IsPartOfProject Value="True"/>
    </Unit>
    <Unit>
      <Filename Value="buildmanager.pas"/>
      <IsPartOfProject Value="True"/>
      <UnitName Value="BuildManager"/>
    </Unit>
    <Unit>
      <Filename Value="basebuildmanager.pas"/>
      <IsPartOfProject Value="True"/>
      <UnitName Value="BaseBuildManager"/>
    </Unit>
    <Unit>
      <Filename Value="idecmdline.pas"/>
      <IsPartOfProject Value="True"/>
      <UnitName Value="IDECmdLine"/>
    </Unit>
    <Unit/>
    <Unit/>
    <Unit/>
    <Unit/>
    <Unit/>
    <Unit>
      <Filename Value="codehelp.pas"/>
      <IsPartOfProject Value="True"/>
    </Unit>
  </Units>

Strangely the original lazbuild.lpi has 2 <BuildModes>, Debug and Release listed, but they are not visible in the Lazarus GUI.
It must be some old setting in a wrong place. Not related to your patch.

Ondrej Pokorny

2019-03-01 09:53

developer   ~0114535

Thanks Juha for testing!

I confirm the bug with BuildModes - I fixed it in 04.patch.

I could not reproduce the empty unit elements bug.

Ondrej Pokorny

2019-03-01 09:53

developer  

xmlcfg-nodeindexes-04.patch (31,032 bytes)
Index: components/ideintf/projectintf.pas
===================================================================
--- components/ideintf/projectintf.pas	(revision 60489)
+++ components/ideintf/projectintf.pas	(working copy)
@@ -247,7 +247,8 @@
     pfLRSFilesInOutputDirectory, // put .lrs files in output directory
     pfUseDefaultCompilerOptions, // load users default compiler options
     pfSaveJumpHistory,
-    pfSaveFoldState
+    pfSaveFoldState,
+    pfCompatibilityMode // use legacy file format to maximize compatibility with old Lazarus versions
     );
   TProjectFlags = set of TProjectFlag;
 
@@ -274,7 +275,8 @@
       'LRSInOutputDirectory',
       'UseDefaultCompilerOptions',
       'SaveJumpHistory',
-      'SaveFoldState'
+      'SaveFoldState',
+      'CompatibilityMode'
     );
   ProjectSessionStorageNames: array[TProjectSessionStorage] of string = (
     'InProjectInfo',
Index: components/lazutils/laz2_xmlcfg.pas
===================================================================
--- components/lazutils/laz2_xmlcfg.pas	(revision 60489)
+++ components/lazutils/laz2_xmlcfg.pas	(working copy)
@@ -16,6 +16,7 @@
 }
 
 {$MODE objfpc}
+{$modeswitch advancedrecords}
 {$H+}
 
 unit Laz2_XMLCfg;
@@ -50,10 +51,23 @@
     procedure SetFilename(const AFilename: String);
   protected
     type
+      TDomNodeArray = array of TDomNode;
       TNodeCache = record
         Node: TDomNode;
+        NodeSearchName: string;
         ChildrenValid: boolean;
-        Children: array of TDomNode; // nodes with NodeName<>'' and sorted
+        Children: TDomNodeArray; // child nodes with NodeName<>'' and sorted
+
+        NodeListName: string;
+        NodeList: TDomNodeArray; // child nodes that are accessed with "name[?]" XPath
+
+      public
+        class procedure GrowArray(var aArray: TDomNodeArray; aCount: Integer); static;
+        procedure RefreshChildren;
+        procedure RefreshChildrenIfNeeded;
+        procedure RefreshNodeList(const ANodeName: string);
+        procedure RefreshNodeListIfNeeded(const ANodeName: string);
+        function AddNodeToList: TDOMNode;
       end;
   protected
     doc: TXMLDocument;
@@ -68,13 +82,15 @@
     procedure ReadXMLFile(out ADoc: TXMLDocument; const AFilename: String); virtual;
     procedure WriteXMLFile(ADoc: TXMLDocument; const AFileName: String); virtual;
     procedure FreeDoc; virtual;
-    procedure SetPathNodeCache(Index: integer; aNode: TDomNode);
+    procedure SetPathNodeCache(Index: integer; aNode: TDomNode; aNodeSearchName: string = '');
     function GetCachedPathNode(Index: integer): TDomNode; inline;
+    function GetCachedPathNode(Index: integer; out aNodeSearchName: string): TDomNode; inline;
     procedure InvalidateCacheTilEnd(StartIndex: integer);
     function InternalFindNode(const APath: String; PathLen: integer;
                               CreateNodes: boolean = false): TDomNode;
     procedure InternalCleanNode(Node: TDomNode);
-    function FindChildNode(PathIndex: integer; const aName: string): TDomNode;
+    function FindChildNode(PathIndex: integer; const aName: string;
+      CreateNodes: boolean = false): TDomNode;
   public
     constructor Create(AOwner: TComponent); override; overload;
     constructor Create(const AFilename: String); overload; // create and load
@@ -109,6 +125,12 @@
     // checks if the path has values, set PathHasValue=true to skip the last part
     function HasPath(const APath: string; PathHasValue: boolean): boolean;
     function HasChildPaths(const APath: string): boolean;
+    function GetChildCount(const APath: string): Integer;
+    function IsLegacyList(const APath: string): Boolean;
+    function GetListItemCount(const APath, AItemName: string; const aLegacyList: Boolean): Integer;
+    function GetListItemXPath(const AName: string; const AIndex: Integer; const aLegacyList: Boolean;
+      const aLegacyList1Based: Boolean = False): string;
+    procedure SetListItemCount(const APath: string; const ACount: Integer; const ALegacyList: Boolean);
     property Modified: Boolean read FModified write FModified;
     procedure InvalidatePathCache;
   published
@@ -150,15 +172,121 @@
   Result:=CompareStr(Node1.NodeName,Node2.NodeName);
 end;
 
+{ TXMLConfig.TNodeCache }
+
+function TXMLConfig.TNodeCache.AddNodeToList: TDOMNode;
+begin
+  Result:=Node.OwnerDocument.CreateElement(NodeListName);
+  Node.AppendChild(Result);
+  SetLength(NodeList, Length(NodeList)+1);
+  NodeList[High(NodeList)]:=Result;
+end;
+
+class procedure TXMLConfig.TNodeCache.GrowArray(var aArray: TDomNodeArray;
+  aCount: Integer);
+var
+  cCount: Integer;
+begin
+  cCount:=length(aArray);
+  if aCount>cCount then begin
+    if cCount<8 then
+      cCount:=8
+    else
+      cCount:=cCount*2;
+    if aCount>cCount then
+      cCount := aCount;
+    SetLength(aArray,cCount);
+  end;
+end;
+
+procedure TXMLConfig.TNodeCache.RefreshChildren;
+var
+  aCount, m: Integer;
+  aChild: TDOMNode;
+begin
+  // collect all children and sort
+  aCount:=0;
+  aChild:=Node.FirstChild;
+  while aChild<>nil do begin
+    if aChild.NodeName<>'' then begin
+      GrowArray(Children, aCount+1);
+      Children[aCount]:=aChild;
+      inc(aCount);
+    end;
+    aChild:=aChild.NextSibling;
+  end;
+  SetLength(Children,aCount);
+  if aCount>1 then
+    MergeSortWithLen(@Children[0],aCount,@CompareDomNodeNames); // sort ascending [0]<[1]
+  for m:=0 to aCount-2 do
+    if Children[m].NodeName=Children[m+1].NodeName then begin
+      // duplicate found: nodes with same name
+      // -> use only the first
+      Children[m+1]:=Children[m];
+    end;
+  ChildrenValid:=true;
+end;
+
+procedure TXMLConfig.TNodeCache.RefreshChildrenIfNeeded;
+begin
+  if not ChildrenValid then
+    RefreshChildren;
+end;
+
+procedure TXMLConfig.TNodeCache.RefreshNodeList(const ANodeName: string);
+var
+  aCount: Integer;
+  aChild: TDOMNode;
+begin
+  aCount:=0;
+  aChild:=Node.FirstChild;
+  while aChild<>nil do
+  begin
+    if aChild.NodeName=ANodeName then
+    begin
+      GrowArray(NodeList, aCount+1);
+      NodeList[aCount]:=aChild;
+      inc(aCount);
+    end;
+    aChild:=aChild.NextSibling;
+  end;
+  SetLength(NodeList,aCount);
+  NodeListName := ANodeName;
+end;
+
+procedure TXMLConfig.TNodeCache.RefreshNodeListIfNeeded(const ANodeName: string
+  );
+begin
+  if NodeListName<>ANodeName then
+    RefreshNodeList(ANodeName);
+end;
+
 // inline
-function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
+function TXMLConfig.GetCachedPathNode(Index: integer; out
+  aNodeSearchName: string): TDomNode;
 begin
   if Index<length(fPathNodeCache) then
-    Result:=fPathNodeCache[Index].Node
-  else
+  begin
+    Result:=fPathNodeCache[Index].Node;
+    aNodeSearchName:=fPathNodeCache[Index].NodeSearchName;
+  end else
+  begin
     Result:=nil;
+    aNodeSearchName:='';
+  end;
 end;
 
+function TXMLConfig.GetChildCount(const APath: string): Integer;
+var
+  Node: TDOMNode;
+begin
+  Node:=FindNode(APath,false);
+  if Node=nil then
+    Result := 0
+  else
+    Result := Node.GetChildCount;
+end;
+
 constructor TXMLConfig.Create(const AFilename: String);
 begin
   //DebugLn(['TXMLConfig.Create ',AFilename]);
@@ -294,6 +422,41 @@
   Result:=StrToExtended(GetValue(APath,''),ADefault);
 end;
 
+function TXMLConfig.GetListItemCount(const APath, AItemName: string;
+  const aLegacyList: Boolean): Integer;
+var
+  Node: TDOMNode;
+  NodeLevel: SizeInt;
+begin
+  if aLegacyList then
+    Result := GetValue(APath+'Count',0)
+  else
+  begin
+    Node:=InternalFindNode(APath,Length(APath));
+    if Node<>nil then
+    begin
+      NodeLevel := Node.GetLevel-1;
+      fPathNodeCache[NodeLevel].RefreshNodeListIfNeeded(AItemName);
+      Result := Length(fPathNodeCache[NodeLevel].NodeList);
+    end else
+      Result := 0;
+  end;
+end;
+
+function TXMLConfig.GetListItemXPath(const AName: string;
+  const AIndex: Integer; const aLegacyList: Boolean;
+  const aLegacyList1Based: Boolean): string;
+begin
+  if ALegacyList then
+  begin
+    if aLegacyList1Based then
+      Result := AName+IntToStr(AIndex+1)
+    else
+      Result := AName+IntToStr(AIndex);
+  end else
+    Result := AName+'['+IntToStr(AIndex+1)+']';
+end;
+
 procedure TXMLConfig.SetValue(const APath, AValue: String);
 var
   Node: TDOMNode;
@@ -450,6 +613,11 @@
   InvalidateCacheTilEnd(0);
 end;
 
+function TXMLConfig.IsLegacyList(const APath: string): Boolean;
+begin
+  Result := GetValue(APath+'Count',-1)>1;
+end;
+
 function TXMLConfig.ExtendedToStr(const e: extended): string;
 begin
   Result := FloatToStr(e, FPointSettings);
@@ -478,8 +646,16 @@
   FreeAndNil(doc);
 end;
 
-procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode);
+function TXMLConfig.GetCachedPathNode(Index: integer): TDomNode;
 var
+  x: string;
+begin
+  Result := GetCachedPathNode(Index, x);
+end;
+
+procedure TXMLConfig.SetPathNodeCache(Index: integer; aNode: TDomNode;
+  aNodeSearchName: string);
+var
   OldLength, NewLength: Integer;
 begin
   OldLength:=length(fPathNodeCache);
@@ -495,9 +671,13 @@
     exit
   else
     InvalidateCacheTilEnd(Index+1);
+  if aNodeSearchName='' then
+    aNodeSearchName:=aNode.NodeName;
   with fPathNodeCache[Index] do begin
     Node:=aNode;
+    NodeSearchName:=aNodeSearchName;
     ChildrenValid:=false;
+    NodeListName:='';
   end;
 end;
 
@@ -510,6 +690,7 @@
       if Node=nil then break;
       Node:=nil;
       ChildrenValid:=false;
+      NodeListName:='';
     end;
   end;
 end;
@@ -517,11 +698,9 @@
 function TXMLConfig.InternalFindNode(const APath: String; PathLen: integer;
   CreateNodes: boolean): TDomNode;
 var
-  NodePath: String;
+  NodePath, NdName: String;
   StartPos, EndPos: integer;
   PathIndex: Integer;
-  Parent: TDOMNode;
-  NdName: DOMString;
   NameLen: Integer;
 begin
   //debugln(['TXMLConfig.InternalFindNode APath="',copy(APath,1,PathLen),'" CreateNodes=',CreateNodes]);
@@ -539,25 +718,15 @@
     NameLen:=EndPos-StartPos;
     if NameLen=0 then break;
     inc(PathIndex);
-    Parent:=Result;
-    Result:=GetCachedPathNode(PathIndex);
-    if Result<>nil then
-      NdName:=Result.NodeName;
+    Result:=GetCachedPathNode(PathIndex,NdName);
     if (Result=nil) or (length(NdName)<>NameLen)
     or not CompareMem(PChar(NdName),@APath[StartPos],NameLen) then begin
       // different path => search
       NodePath:=copy(APath,StartPos,NameLen);
-      Result:=FindChildNode(PathIndex-1,NodePath);
-      if Result=nil then begin
-        if not CreateNodes then exit;
-        // create missing node
-        Result:=Doc.CreateElement(NodePath);
-        Parent.AppendChild(Result);
-        fPathNodeCache[PathIndex-1].ChildrenValid:=false;
-        InvalidateCacheTilEnd(PathIndex);
-        if EndPos>PathLen then exit;
-      end;
-      SetPathNodeCache(PathIndex,Result);
+      Result:=FindChildNode(PathIndex-1,NodePath,CreateNodes);
+      if Result=nil then
+        Exit;
+      SetPathNodeCache(PathIndex,Result,NodePath);
     end;
     StartPos:=EndPos+1;
     if StartPos>PathLen then exit;
@@ -581,62 +750,56 @@
   end;
 end;
 
-function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string
-  ): TDomNode;
+function TXMLConfig.FindChildNode(PathIndex: integer; const aName: string;
+  CreateNodes: boolean): TDomNode;
 var
-  aParent, aChild: TDOMNode;
-  aCount: Integer;
-  NewLength: Integer;
   l, r, m: Integer;
-  cmp: Integer;
+  cmp, BrPos: Integer;
+  NodeName: string;
 begin
-  with fPathNodeCache[PathIndex] do begin
-    if not ChildrenValid then begin
-      // collect all children and sort
-      aParent:=Node;
-      aCount:=0;
-      aChild:=aParent.FirstChild;
-      while aChild<>nil do begin
-        if aChild.NodeName<>'' then begin
-          if aCount=length(Children) then begin
-            NewLength:=length(Children);
-            if NewLength<8 then
-              NewLength:=8
-            else
-              NewLength:=NewLength*2;
-            SetLength(Children,NewLength);
-          end;
-          Children[aCount]:=aChild;
-          inc(aCount);
-        end;
-        aChild:=aChild.NextSibling;
-      end;
-      SetLength(Children,aCount);
-      if aCount>1 then
-        MergeSortWithLen(@Children[0],aCount,@CompareDomNodeNames); // sort ascending [0]<[1]
-      for m:=0 to aCount-2 do
-        if Children[m].NodeName=Children[m+1].NodeName then begin
-          // duplicate found: nodes with same name
-          // -> use only the first
-          Children[m+1]:=Children[m];
-        end;
-      ChildrenValid:=true;
+  BrPos := Pos('[', aName);
+  if (Length(aName)>=BrPos+2) and (aName[Length(aName)]=']')
+  and TryStrToInt(Trim(Copy(aName, BrPos+1, Length(aName)-BrPos-1)), m) then
+  begin
+    // support XPath in format "name[?]"
+    NodeName := Trim(Copy(aName, 1, BrPos-1));
+    fPathNodeCache[PathIndex].RefreshNodeListIfNeeded(NodeName);
+    if m<=0 then
+      raise Exception.CreateFmt('Invalid node index in XPath descriptor "%s".', [aName])
+    else if (m<=Length(fPathNodeCache[PathIndex].NodeList)) then
+      Result:=fPathNodeCache[PathIndex].NodeList[m-1]
+    else if CreateNodes then
+    begin
+      for l := Length(fPathNodeCache[PathIndex].NodeList)+1 to m do
+        Result := fPathNodeCache[PathIndex].AddNodeToList;
+      InvalidateCacheTilEnd(PathIndex+1);
     end;
+  end else
+  begin
+    fPathNodeCache[PathIndex].RefreshChildrenIfNeeded;
 
     // binary search
     l:=0;
-    r:=length(Children)-1;
+    r:=length(fPathNodeCache[PathIndex].Children)-1;
     while l<=r do begin
       m:=(l+r) shr 1;
-      cmp:=CompareStr(aName,Children[m].NodeName);
+      cmp:=CompareStr(aName,fPathNodeCache[PathIndex].Children[m].NodeName);
       if cmp<0 then
         r:=m-1
       else if cmp>0 then
         l:=m+1
       else
-        exit(Children[m]);
+        exit(fPathNodeCache[PathIndex].Children[m]);
     end;
-    Result:=nil;
+    if CreateNodes then
+    begin
+      // create missing node
+      Result:=Doc.CreateElement(aName);
+      fPathNodeCache[PathIndex].Node.AppendChild(Result);
+      fPathNodeCache[PathIndex].ChildrenValid:=false;
+      InvalidateCacheTilEnd(PathIndex+1);
+    end else
+      Result:=nil;
   end;
 end;
 
@@ -686,6 +849,13 @@
   {$IFDEF MEM_CHECK}CheckHeapWrtMemCnt('TXMLConfig.SetFilename END');{$ENDIF}
 end;
 
+procedure TXMLConfig.SetListItemCount(const APath: string;
+  const ACount: Integer; const ALegacyList: Boolean);
+begin
+  if ALegacyList then
+    SetDeleteValue(APath+'Count',ACount,0)
+end;
+
 procedure TXMLConfig.CreateConfigNode;
 var
   cfg: TDOMElement;
Index: ide/frames/project_misc_options.lfm
===================================================================
--- ide/frames/project_misc_options.lfm	(revision 60489)
+++ ide/frames/project_misc_options.lfm	(working copy)
@@ -118,13 +118,13 @@
   end
   object ResourceGroupBox: TGroupBox
     AnchorSideLeft.Control = Owner
-    AnchorSideTop.Control = LRSInOutputDirCheckBox
+    AnchorSideTop.Control = CompatibilityModeCheckBox
     AnchorSideTop.Side = asrBottom
     AnchorSideRight.Control = Owner
     AnchorSideRight.Side = asrBottom
     Left = 0
     Height = 81
-    Top = 231
+    Top = 256
     Width = 536
     Anchors = [akTop, akLeft, akRight]
     BorderSpacing.Top = 6
@@ -139,7 +139,7 @@
     ChildSizing.ControlsPerLine = 1
     ClientHeight = 61
     ClientWidth = 532
-    TabOrder = 9
+    TabOrder = 10
     object UseFPCResourcesRadioButton: TRadioButton
       Left = 6
       Height = 25
@@ -183,7 +183,7 @@
     AnchorSideTop.Side = asrCenter
     Left = 0
     Height = 15
-    Top = 331
+    Top = 356
     Width = 83
     Caption = 'PathDelimLabel'
     ParentColor = False
@@ -196,7 +196,7 @@
     AnchorSideRight.Side = asrBottom
     Left = 0
     Height = 3
-    Top = 318
+    Top = 343
     Width = 536
     Anchors = [akTop, akLeft, akRight]
     BorderSpacing.Top = 6
@@ -209,12 +209,12 @@
     AnchorSideRight.Side = asrBottom
     Left = 89
     Height = 23
-    Top = 327
+    Top = 352
     Width = 259
     BorderSpacing.Left = 6
     BorderSpacing.Top = 6
     ItemHeight = 15
-    TabOrder = 10
+    TabOrder = 11
     Text = 'PathDelimComboBox'
   end
   object MainUnitHasScaledStatementCheckBox: TCheckBox
@@ -231,4 +231,18 @@
     ShowHint = True
     TabOrder = 4
   end
+  object CompatibilityModeCheckBox: TCheckBox
+    AnchorSideLeft.Control = Owner
+    AnchorSideTop.Control = LRSInOutputDirCheckBox
+    AnchorSideTop.Side = asrBottom
+    Left = 0
+    Height = 19
+    Top = 231
+    Width = 175
+    BorderSpacing.Top = 6
+    Caption = 'CompatibilityModeCheckBox'
+    ParentShowHint = False
+    ShowHint = True
+    TabOrder = 9
+  end
 end
Index: ide/frames/project_misc_options.pas
===================================================================
--- ide/frames/project_misc_options.pas	(revision 60489)
+++ ide/frames/project_misc_options.pas	(working copy)
@@ -25,6 +25,7 @@
     Bevel2: TBevel;
     LRSInOutputDirCheckBox: TCheckBox;
     MainUnitHasCreateFormStatementsCheckBox: TCheckBox;
+    CompatibilityModeCheckBox: TCheckBox;
     MainUnitHasTitleStatementCheckBox: TCheckBox;
     MainUnitHasScaledStatementCheckBox: TCheckBox;
     MainUnitHasUsesSectionForAllUnitsCheckBox: TCheckBox;
@@ -68,6 +69,8 @@
   MainUnitHasTitleStatementCheckBox.Hint := lisIdeMaintainsTheTitleInMainUnit;
   MainUnitHasScaledStatementCheckBox.Caption := lisMainUnitHasApplicationScaledStatement;
   MainUnitHasScaledStatementCheckBox.Hint := lisIdeMaintainsScaledInMainUnit;
+  CompatibilityModeCheckBox.Caption := lisLPICompatibilityModeCheckBox;
+  CompatibilityModeCheckBox.Hint := lisLPICompatibilityModeCheckBoxHint;
   RunnableCheckBox.Caption := lisProjectIsRunnable;
   RunnableCheckBox.Hint := lisProjectIsRunnableHint;
   UseDesignTimePkgsCheckBox.Caption := lisUseDesignTimePackages;
@@ -96,6 +99,7 @@
     MainUnitHasCreateFormStatementsCheckBox.Checked := (pfMainUnitHasCreateFormStatements in Flags);
     MainUnitHasTitleStatementCheckBox.Checked := (pfMainUnitHasTitleStatement in Flags);
     MainUnitHasScaledStatementCheckBox.Checked := (pfMainUnitHasScaledStatement in Flags);
+    CompatibilityModeCheckBox.Checked := (pfCompatibilityMode in Flags);
     RunnableCheckBox.Checked := (pfRunnable in Flags);
     UseDesignTimePkgsCheckBox.Checked := (pfUseDesignTimePackages in Flags);
     AlwaysBuildCheckBox.Checked := (pfAlwaysBuild in Flags);
@@ -140,6 +144,8 @@
                  MainUnitHasTitleStatementCheckBox.Checked);
   SetProjectFlag(pfMainUnitHasScaledStatement,
                  MainUnitHasScaledStatementCheckBox.Checked);
+  SetProjectFlag(pfCompatibilityMode,
+                 CompatibilityModeCheckBox.Checked);
   SetProjectFlag(pfRunnable, RunnableCheckBox.Checked);
   SetProjectFlag(pfUseDesignTimePackages, UseDesignTimePkgsCheckBox.Checked);
   SetProjectFlag(pfAlwaysBuild, AlwaysBuildCheckBox.Checked);
Index: ide/imexportcompileropts.pas
===================================================================
--- ide/imexportcompileropts.pas	(revision 60489)
+++ ide/imexportcompileropts.pas	(working copy)
@@ -262,7 +262,7 @@
 begin
   Result := OpenXML(Filename);
   if Result <> mrOK then Exit;
-  Project1.BuildModes.SaveProjOptsToXMLConfig(fXMLConfig, '', False);
+  Project1.BuildModes.SaveProjOptsToXMLConfig(fXMLConfig, '', False, True);
   fXMLConfig.Flush;
   ShowMessageFmt(lisSuccessfullyExportedBuildModes, [Project1.BuildModes.Count, Filename]);
 end;
Index: ide/lazarusidestrconsts.pas
===================================================================
--- ide/lazarusidestrconsts.pas	(revision 60489)
+++ ide/lazarusidestrconsts.pas	(working copy)
@@ -2692,6 +2692,8 @@
   lisIdeMaintainsTheTitleInMainUnit = 'The IDE maintains the title in main unit.';
   lisMainUnitHasApplicationScaledStatement = 'Main unit has Application.Scaled statement';
   lisIdeMaintainsScaledInMainUnit = 'The IDE maintains Application.Scaled (Hi-DPI) in main unit.';
+  lisLPICompatibilityModeCheckBox = 'Maximize compatibility of project files (LPI and LPS)';
+  lisLPICompatibilityModeCheckBoxHint = 'Check this if you want to open your project in legacy (2.0 and older) Lazarus versions.';
   lisProjectIsRunnable = 'Project is runnable';
   lisProjectIsRunnableHint = 'Generates a binary executable which can be run.';
   lisUseDesignTimePackages = 'Use design time packages';
Index: ide/project.pp
===================================================================
--- ide/project.pp	(revision 60489)
+++ ide/project.pp	(working copy)
@@ -589,7 +589,7 @@
     procedure LoadFromXMLConfig(XMLConfig: TXMLConfig; const Path: string);
     procedure SaveMacroValuesAtOldPlace(XMLConfig: TXMLConfig; const Path: string);
     procedure SaveToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
-                              IsDefault: Boolean; var Cnt: integer);
+                              IsDefault, ALegacyList: Boolean; var Cnt: integer);
     function GetCaption: string; override;
     function GetIndex: integer; override;
   public
@@ -655,9 +655,9 @@
     procedure LoadSessionFromXMLConfig(XMLConfig: TXMLConfig; const Path: string;
                                        LoadAllOptions: boolean);
     procedure SaveProjOptsToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
-                                      SaveSession: boolean);
+                                      SaveSession, ALegacyList: boolean);
     procedure SaveSessionOptsToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
-                                         SaveSession: boolean);
+                                         SaveSession, ALegacyList: boolean);
   public
     property Items[Index: integer]: TProjectBuildMode read GetItems; default;
     property ChangeStamp: integer read FChangeStamp;
@@ -785,6 +785,7 @@
     function GetSourceDirectories: TFileReferenceList;
     function GetTargetFilename: string;
     function GetUnits(Index: integer): TUnitInfo;
+    function GetUseLegacyLists: Boolean;
     function JumpHistoryCheckPosition(
                                 APosition:TProjectJumpHistoryPosition): boolean;
     function OnUnitFileBackup(const Filename: string): TModalResult;
@@ -1058,6 +1059,7 @@
     property EnableI18NForLFM: boolean read FEnableI18NForLFM write SetEnableI18NForLFM;
     property I18NExcludedIdentifiers: TStrings read FI18NExcludedIdentifiers;
     property I18NExcludedOriginals: TStrings read FI18NExcludedOriginals;
+    property UseLegacyLists: Boolean read GetUseLegacyLists;
     property ForceUpdatePoFiles: Boolean read FForceUpdatePoFiles write FForceUpdatePoFiles;
     property FirstAutoRevertLockedUnit: TUnitInfo read GetFirstAutoRevertLockedUnit;
     property FirstLoadedUnit: TUnitInfo read GetFirstLoadedUnit;
@@ -1134,7 +1136,7 @@
 implementation
 
 const
-  ProjectInfoFileVersion = 11;
+  ProjectInfoFileVersion = 12;
   ProjOptionsPath = 'ProjectOptions/';
 
 
@@ -2813,12 +2815,13 @@
   SubPath: String;
   NewUnitFilename: String;
   OldUnitInfo: TUnitInfo;
-  MergeUnitInfo: Boolean;
+  MergeUnitInfo, LegacyList: Boolean;
 begin
   {$IFDEF IDE_MEM_CHECK}CheckHeapWrtMemCnt('TProject.ReadProject D reading units');{$ENDIF}
-  NewUnitCount:=FXMLConfig.GetValue(Path+'Units/Count',0);
+  LegacyList:=(FFileVersion<=11) or FXMLConfig.IsLegacyList(Path+'Units/');
+  NewUnitCount:=FXMLConfig.GetListItemCount(Path+'Units/', 'Unit', LegacyList);
   for i := 0 to NewUnitCount - 1 do begin
-    SubPath:=Path+'Units/Unit'+IntToStr(i)+'/';
+    SubPath:=Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, LegacyList)+'/';
     NewUnitFilename:=FXMLConfig.GetValue(SubPath+'Filename/Value','');
     OnLoadSaveFilename(NewUnitFilename,true);
     // load unit and add it
@@ -2867,7 +2870,7 @@
 const
   Path = ProjOptionsPath;
 begin
-  if (FFileVersion=0) and (FXMLConfig.GetValue(Path+'Units/Count',0)=0) then
+  if (FFileVersion=0) and (FXMLConfig.GetListItemCount(Path+'Units/', 'Unit', UseLegacyLists)=0) then
     if IDEMessageDialog(lisStrangeLpiFile,
         Format(lisTheFileDoesNotLookLikeALpiFile, [ProjectInfoFile]),
         mtConfirmation,[mbIgnore,mbAbort])<>mrIgnore
@@ -3149,10 +3152,10 @@
   for i:=0 to UnitCount-1 do
     if UnitMustBeSaved(Units[i],FProjectWriteFlags,SaveSession) then begin
       Units[i].SaveToXMLConfig(FXMLConfig,
-        Path+'Units/Unit'+IntToStr(SaveUnitCount)+'/',True,SaveSession,fCurStorePathDelim);
+        Path+'Units/'+FXMLConfig.GetListItemXPath('Unit', i, UseLegacyLists)+'/',True,SaveSession,fCurStorePathDelim);
       inc(SaveUnitCount);
     end;
-  FXMLConfig.SetDeleteValue(Path+'Units/Count',SaveUnitCount,0);
+  FXMLConfig.SetListItemCount(Path+'Units/',SaveUnitCount,UseLegacyLists);
 end;
 
 procedure TProject.SaveOtherDefines(const Path: string);
@@ -3197,6 +3200,7 @@
 var
   CurFlags: TProjectWriteFlags;
 begin
+  FFileVersion:=ProjectInfoFileVersion;
   // format
   FXMLConfig.SetValue(Path+'Version/Value',ProjectInfoFileVersion);
   FXMLConfig.SetDeleteValue(Path+'PathDelim/Value',PathDelimSwitchToDelim[fCurStorePathDelim],'/');
@@ -3232,7 +3236,7 @@
   // save custom data
   SaveStringToStringTree(FXMLConfig,CustomData,Path+'CustomData/');
   // Save the macro values and compiler options
-  BuildModes.SaveProjOptsToXMLConfig(FXMLConfig, Path, FSaveSessionInLPI);
+  BuildModes.SaveProjOptsToXMLConfig(FXMLConfig, Path, FSaveSessionInLPI, UseLegacyLists);
   BuildModes.SaveSharedMatrixOptions(Path);
   if FSaveSessionInLPI then
     BuildModes.SaveSessionData(Path);
@@ -3287,6 +3291,7 @@
 const
   Path = 'ProjectSession/';
 begin
+  FFileVersion:=ProjectInfoFileVersion;
   fCurStorePathDelim:=SessionStorePathDelim;
   FXMLConfig.SetDeleteValue(Path+'PathDelim/Value',
                           PathDelimSwitchToDelim[fCurStorePathDelim],'/');
@@ -3293,7 +3298,7 @@
   FXMLConfig.SetValue(Path+'Version/Value',ProjectInfoFileVersion);
 
   // Save the session build modes
-  BuildModes.SaveSessionOptsToXMLConfig(FXMLConfig, Path, True);
+  BuildModes.SaveSessionOptsToXMLConfig(FXMLConfig, Path, True, UseLegacyLists);
   BuildModes.SaveSessionData(Path);
   // save all units
   SaveUnits(Path,true);
@@ -4453,6 +4458,11 @@
   end;
 end;
 
+function TProject.GetUseLegacyLists: Boolean;
+begin
+  Result := (FFileVersion<=11) or (pfCompatibilityMode in Flags);
+end;
+
 function TProject.HasProjectInfoFileChangedOnDisk: boolean;
 var
   AnUnitInfo: TUnitInfo;
@@ -6814,13 +6824,13 @@
   XMLConfig.SetDeleteValue(Path+'Count',Cnt,0);
 end;
 
-procedure TProjectBuildMode.SaveToXMLConfig(XMLConfig: TXMLConfig; const Path: string;
-  IsDefault: Boolean; var Cnt: integer);
+procedure TProjectBuildMode.SaveToXMLConfig(XMLConfig: TXMLConfig;
+  const Path: string; IsDefault, ALegacyList: Boolean; var Cnt: integer);
 var
   SubPath: String;
 begin
+  SubPath:=Path+'BuildModes/'+XMLConfig.GetListItemXPath('Item', Cnt, ALegacyList, True)+'/';
   inc(Cnt);
-  SubPath:=Path+'BuildModes/Item'+IntToStr(Cnt)+'/';
   XMLConfig.SetDeleteValue(SubPath+'Name',Identifier,'');
   if IsDefault then
     XMLConfig.SetDeleteValue(SubPath+'Default',True,false)
@@ -7198,10 +7208,12 @@
   i: Integer;
   Ident, SubPath: String;
   CurMode: TProjectBuildMode;
+  LegacyList: Boolean;
 begin
+  LegacyList := FXMLConfig.IsLegacyList(Path);
   for i:=FromIndex to ToIndex do
   begin
-    SubPath:=Path+'Item'+IntToStr(i)+'/';
+    SubPath:=Path+FXMLConfig.GetListItemXPath('Item', i-1, LegacyList, True)+'/';
     Ident:=FXMLConfig.GetValue(SubPath+'Name','');
     CurMode:=Add(Ident);                     // add another mode
     CurMode.InSession:=InSession;
@@ -7231,13 +7243,15 @@
 var
   i: Integer;
   SubPath: String;
+  IsLegacyList: Boolean;
 begin
   // First default mode.
   LoadMacroValues(Path+'MacroValues/', Items[0]);
+  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes/');
   // Iterate rest of the modes.
   for i:=2 to Cnt do
   begin
-    SubPath:=Path+'BuildModes/Item'+IntToStr(i)+'/';
+    SubPath:=Path+'BuildModes/'+FXMLConfig.GetListItemXPath('Item', i-1, IsLegacyList, True);
     LoadMacroValues(SubPath+'MacroValues/', Items[i-1]);
   end;
 end;
@@ -7279,15 +7293,17 @@
 // Load for project
 var
   Cnt: Integer;
+  IsLegacyList: Boolean;
 begin
   FXMLConfig := XMLConfig;
 
-  Cnt:=FXMLConfig.GetValue(Path+'BuildModes/Count',0);
+  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes/');
+  Cnt:=FXMLConfig.GetListItemCount(Path+'BuildModes/', 'Item', IsLegacyList);
   if Cnt>0 then begin
     // Project default mode is stored at the old XML path for backward compatibility.
     // Testing the 'Default' XML attribute is not needed because the first mode
     // is always default.
-    Items[0].Identifier:=FXMLConfig.GetValue(Path+'BuildModes/Item1/Name', '');
+    Items[0].Identifier:=FXMLConfig.GetValue(Path+'BuildModes/'+XMLConfig.GetListItemXPath('Item', 0, IsLegacyList, True)+'/Name', '');
     Items[0].CompilerOptions.LoadFromXMLConfig(FXMLConfig, 'CompilerOptions/');
     LoadOtherCompilerOpts(Path+'BuildModes/', 2, Cnt, False);
     LoadAllMacroValues(Path+'MacroValues/', Cnt);
@@ -7303,6 +7319,7 @@
 // Load for session
 var
   Cnt: Integer;
+  IsLegacyList: Boolean;
 begin
   FXMLConfig := XMLConfig;
 
@@ -7310,7 +7327,8 @@
     // load matrix options
     SessionMatrixOptions.LoadFromXMLConfig(FXMLConfig, Path+'BuildModes/SessionMatrixOptions/');
 
-  Cnt:=FXMLConfig.GetValue(Path+'BuildModes/Count',0);
+  IsLegacyList := FXMLConfig.IsLegacyList(Path+'BuildModes/');
+  Cnt:=FXMLConfig.GetListItemCount(Path+'BuildModes/', 'Item', IsLegacyList);
   if Cnt>0 then begin
     // Add a new mode for session compiler options.
     LoadOtherCompilerOpts(Path+'BuildModes/', 1, Cnt, True);
@@ -7362,7 +7380,7 @@
 
 // SaveToXMLConfig itself
 procedure TProjectBuildModes.SaveProjOptsToXMLConfig(XMLConfig: TXMLConfig;
-  const Path: string; SaveSession: boolean);
+  const Path: string; SaveSession, ALegacyList: boolean);
 var
   i, Cnt: Integer;
 begin
@@ -7377,12 +7395,12 @@
   Cnt:=0;
   for i:=0 to Count-1 do
     if SaveSession or not Items[i].InSession then
-      Items[i].SaveToXMLConfig(FXMLConfig, Path, i=0, Cnt);
-  FXMLConfig.SetDeleteValue(Path+'BuildModes/Count',Cnt,0);
+      Items[i].SaveToXMLConfig(FXMLConfig, Path, i=0, ALegacyList, Cnt);
+  FXMLConfig.SetListItemCount(Path+'BuildModes',Cnt,ALegacyList);
 end;
 
 procedure TProjectBuildModes.SaveSessionOptsToXMLConfig(XMLConfig: TXMLConfig;
-  const Path: string; SaveSession: boolean);
+  const Path: string; SaveSession, ALegacyList: boolean);
 var
   i, Cnt: Integer;
 begin
@@ -7391,8 +7409,8 @@
   Cnt:=0;
   for i:=0 to Count-1 do
     if Items[i].InSession and SaveSession then
-      Items[i].SaveToXMLConfig(FXMLConfig, Path, false, Cnt);
-  FXMLConfig.SetDeleteValue(Path+'BuildModes/Count',Cnt,0);
+      Items[i].SaveToXMLConfig(FXMLConfig, Path, false, ALegacyList, Cnt);
+  FXMLConfig.SetListItemCount(Path+'BuildModes',Cnt,ALegacyList);
 end;
 
 

Juha Manninen

2019-03-02 14:20

developer   ~0114561

Ok, it was a real bug with BuildModes. Now it works.

I think this should be applied. It must be done at some point anyway. It will cause a minor temporary invonvenience but that is OK.

Juha Manninen

2019-03-15 17:17

developer   ~0114841

I applied Ondrej's xmlcfg-nodeindexes-04.patch in r60683.
Please test everybody.
I will keep this issue open for a while. It is a major change and can potentially cause regression bugs.

Juha Manninen

2019-03-17 11:20

developer   ~0114895

No complaints so far. Resolving...

Ondrej Pokorny

2019-03-24 10:25

developer   ~0115012

Thank you.

wp

2019-04-03 15:16

developer   ~0115193

Still not quite happy with it...

I post a lot of code in the forum, and since I work with Laz trunk and usually forget to activate the compatibility mode the files are written in the new format and thus not readable by users with legacy Lazarus versions.

I would appreciate to have an option in the environment to turn compatibility on even in trunk

Cyrax

2019-04-14 08:59

reporter   ~0115491

https://bugs.freepascal.org/view.php?id=35377

Issue History

Date Modified Username Field Change
2012-08-30 11:05 carli New Issue
2012-08-30 11:15 Jonas Maebe Project FPC => Lazarus
2019-02-24 20:23 Ondrej Pokorny File Added: xmlcfg-nodeindexes-01.patch
2019-02-24 20:31 Ondrej Pokorny Note Added: 0114385
2019-02-25 00:45 Cyrax Note Added: 0114394
2019-02-25 10:25 Martok Note Added: 0114397
2019-02-25 11:54 errno Note Added: 0114399
2019-02-25 12:05 Martin Friebe Note Added: 0114400
2019-02-25 12:38 Ondrej Pokorny Note Added: 0114402
2019-02-25 13:44 wp Note Added: 0114403
2019-02-25 13:45 wp Note Edited: 0114403 View Revisions
2019-02-25 14:05 Ondrej Pokorny Note Added: 0114406
2019-02-25 14:26 wp Note Added: 0114408
2019-02-25 16:53 Martin Friebe Note Added: 0114412
2019-02-25 18:30 Ondrej Pokorny Note Added: 0114419
2019-02-26 08:28 Ondrej Pokorny File Added: xmlcfg-nodeindexes-02.patch
2019-02-26 08:32 Ondrej Pokorny Note Added: 0114459
2019-02-26 15:19 Martin Friebe Note Added: 0114463
2019-02-26 19:40 Martok Note Added: 0114468
2019-02-26 20:37 Martin Friebe Note Added: 0114471
2019-02-27 07:44 Ondrej Pokorny Note Added: 0114481
2019-02-27 07:45 Ondrej Pokorny Note Edited: 0114481 View Revisions
2019-02-27 12:57 Ondrej Pokorny Note Added: 0114487
2019-02-27 12:58 Ondrej Pokorny File Added: xmlcfg-nodeindexes-03.patch
2019-02-27 17:07 Martok Note Added: 0114491
2019-03-01 09:13 Juha Manninen Note Added: 0114534
2019-03-01 09:53 Ondrej Pokorny Note Added: 0114535
2019-03-01 09:53 Ondrej Pokorny File Added: xmlcfg-nodeindexes-04.patch
2019-03-02 14:20 Juha Manninen Note Added: 0114561
2019-03-15 17:11 Juha Manninen Assigned To => Juha Manninen
2019-03-15 17:11 Juha Manninen Status new => assigned
2019-03-15 17:17 Juha Manninen LazTarget => -
2019-03-15 17:17 Juha Manninen Note Added: 0114841
2019-03-15 17:17 Juha Manninen Status assigned => feedback
2019-03-17 11:20 Juha Manninen Fixed in Revision => r60683
2019-03-17 11:20 Juha Manninen Note Added: 0114895
2019-03-17 11:20 Juha Manninen Status feedback => resolved
2019-03-17 11:20 Juha Manninen Resolution open => fixed
2019-03-17 13:10 wp Status resolved => assigned
2019-03-17 13:10 wp Resolution fixed => reopened
2019-03-17 13:10 wp Status assigned => feedback
2019-03-19 00:40 wp Status feedback => resolved
2019-03-19 00:40 wp Resolution reopened => fixed
2019-03-24 10:25 Ondrej Pokorny Note Added: 0115012
2019-03-24 17:11 Juha Manninen Relationship added related to 0035267
2019-04-03 15:16 wp Note Added: 0115193
2019-04-03 15:16 wp Status resolved => assigned
2019-04-03 15:16 wp Resolution fixed => reopened
2019-04-03 15:16 wp Status assigned => feedback
2019-04-14 08:59 Cyrax Note Added: 0115491
2019-04-14 09:37 Juha Manninen Relationship added related to 0035377